add apikey
This commit is contained in:
@@ -23,7 +23,7 @@ Example
|
||||
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import asdict, dataclass
|
||||
|
||||
import boto3
|
||||
from aws_lambda_powertools import Logger, Tracer
|
||||
@@ -34,14 +34,19 @@ from aws_lambda_powertools.utilities.data_classes.api_gateway_authorizer_event i
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from botocore.endpoint_provider import Enum
|
||||
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer, KeyPair
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from cognito import get_user
|
||||
from settings import USER_TABLE
|
||||
|
||||
APIKEY_PREFIX = 'sk-'
|
||||
|
||||
tracer = Tracer()
|
||||
logger = Logger(__name__)
|
||||
idp_client = boto3.client('cognito-idp')
|
||||
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||
collect = DynamoDBCollection(user_layer)
|
||||
|
||||
|
||||
@tracer.capture_lambda_handler
|
||||
@@ -53,16 +58,8 @@ def lambda_handler(event: APIGatewayAuthorizerEventV2, context: LambdaContext):
|
||||
if not bearer:
|
||||
return APIGatewayAuthorizerResponseV2(authorize=False).asdict()
|
||||
|
||||
if bearer.auth_type == TokenType.USER_TOKEN:
|
||||
user = get_user(bearer.token, idp_client=idp_client)
|
||||
|
||||
if user:
|
||||
return APIGatewayAuthorizerResponseV2(
|
||||
authorize=True,
|
||||
context=dict(user=user),
|
||||
).asdict()
|
||||
|
||||
return APIGatewayAuthorizerResponseV2(authorize=False).asdict()
|
||||
kwargs = asdict(_authorizer(bearer))
|
||||
return APIGatewayAuthorizerResponseV2(**kwargs).asdict()
|
||||
|
||||
|
||||
class TokenType(str, Enum):
|
||||
@@ -76,6 +73,25 @@ class BearerToken:
|
||||
token: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Authorizer:
|
||||
authorize: bool = False
|
||||
context: dict | None = None
|
||||
|
||||
|
||||
def _authorizer(bearer: BearerToken) -> Authorizer:
|
||||
try:
|
||||
match bearer.auth_type:
|
||||
case TokenType.USER_TOKEN:
|
||||
user = get_user(bearer.token, idp_client=idp_client)
|
||||
return Authorizer(True, {'user': user})
|
||||
case TokenType.API_KEY:
|
||||
apikey = collect.get_item(KeyPair('apikey', bearer.token))
|
||||
return Authorizer(True, {'tenant': apikey['tenant']})
|
||||
except Exception:
|
||||
return Authorizer()
|
||||
|
||||
|
||||
def _parse_bearer_token(s: str) -> BearerToken | None:
|
||||
"""Parses and identifies a bearer token as either an API key or a user token."""
|
||||
try:
|
||||
|
||||
@@ -8,7 +8,7 @@ from tqdm import tqdm
|
||||
from boto3clients import dynamodb_client
|
||||
|
||||
elastic_client = Elasticsearch('http://127.0.0.1:9200')
|
||||
files = (
|
||||
jsonl_files = (
|
||||
'test-orders.jsonl',
|
||||
'test-users.jsonl',
|
||||
'test-enrollments.jsonl',
|
||||
@@ -16,7 +16,7 @@ files = (
|
||||
)
|
||||
|
||||
|
||||
def put_item(item: dict, table_name: str, *, dynamodb_client) -> bool:
|
||||
def put_item(item: dict, table_name: str, /, dynamodb_client) -> bool:
|
||||
try:
|
||||
dynamodb_client.put_item(
|
||||
TableName=table_name,
|
||||
@@ -28,7 +28,7 @@ def put_item(item: dict, table_name: str, *, dynamodb_client) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def scan_table(table_name: str, *, dynamodb_client, **kwargs) -> Generator:
|
||||
def scan_table(table_name: str, /, dynamodb_client, **kwargs) -> Generator:
|
||||
try:
|
||||
r = dynamodb_client.scan(TableName=table_name, **kwargs)
|
||||
except Exception:
|
||||
@@ -45,6 +45,31 @@ def scan_table(table_name: str, *, dynamodb_client, **kwargs) -> Generator:
|
||||
)
|
||||
|
||||
|
||||
class Elastic:
|
||||
def __init__(self, client: Elasticsearch) -> None:
|
||||
self.client = client
|
||||
|
||||
def index_item(
|
||||
self,
|
||||
id: str,
|
||||
index: str,
|
||||
doc: dict,
|
||||
):
|
||||
return self.client.index(
|
||||
index=index,
|
||||
id=id,
|
||||
document=_serialize_python_type(doc),
|
||||
)
|
||||
|
||||
def delete_index(self, index: str) -> bool:
|
||||
try:
|
||||
self.client.indices.delete(index=index)
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def _serialize_python_type(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {k: _serialize_python_type(v) for k, v in value.items()}
|
||||
@@ -58,53 +83,27 @@ def _serialize_python_type(value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def index_item(
|
||||
id: str,
|
||||
index: str,
|
||||
doc: dict,
|
||||
*,
|
||||
elastic_client: Elasticsearch,
|
||||
):
|
||||
return elastic_client.index(
|
||||
index=index,
|
||||
id=id,
|
||||
document=_serialize_python_type(doc),
|
||||
)
|
||||
|
||||
|
||||
def delete_index(index: str, *, elastic_client: Elasticsearch) -> bool:
|
||||
try:
|
||||
elastic_client.indices.delete(index=index)
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
for file in tqdm(files, desc='Processing files'):
|
||||
elastic = Elastic(elastic_client)
|
||||
|
||||
for file in tqdm(jsonl_files, desc='Processing files'):
|
||||
with jsonl.readlines(f'seeds/{file}') as lines:
|
||||
table_name = file.removesuffix('.jsonl')
|
||||
|
||||
for line in tqdm(lines, desc=f'Processing lines in {file}'):
|
||||
put_item(line, table_name, dynamodb_client=dynamodb_client)
|
||||
put_item(line, table_name, dynamodb_client)
|
||||
|
||||
for file in tqdm(files, desc='Scanning tables'):
|
||||
for file in tqdm(jsonl_files, desc='Scanning tables'):
|
||||
table_name = file.removesuffix('.jsonl')
|
||||
delete_index(table_name, elastic_client=elastic_client)
|
||||
elastic.delete_index(table_name)
|
||||
|
||||
for record in tqdm(
|
||||
scan_table(
|
||||
table_name,
|
||||
dynamodb_client=dynamodb_client,
|
||||
dynamodb_client,
|
||||
FilterExpression='sk = :sk',
|
||||
ExpressionAttributeValues={':sk': {'S': '0'}},
|
||||
),
|
||||
desc=f'Indexing {table_name}',
|
||||
):
|
||||
index_item(
|
||||
id=record['id'],
|
||||
index=table_name,
|
||||
doc=record,
|
||||
elastic_client=elastic_client,
|
||||
)
|
||||
elastic.index_item(id=record['id'], index=table_name, doc=record)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
class UserNotFound(Exception): ...
|
||||
|
||||
|
||||
def get_user(access_token: str, *, idp_client) -> dict | None:
|
||||
"""Gets the user attributes and metadata for a user."""
|
||||
try:
|
||||
user = idp_client.get_user(AccessToken=access_token)
|
||||
except idp_client.exceptions.ClientError:
|
||||
return None
|
||||
raise UserNotFound()
|
||||
else:
|
||||
return {attr['Name']: attr['Value'] for attr in user['UserAttributes']}
|
||||
|
||||
@@ -43,11 +43,9 @@ class CoursePayload(BaseModel):
|
||||
|
||||
@router.post('/', compress=True, tags=['Course'])
|
||||
def post_course(payload: CoursePayload):
|
||||
org = Org(id='*', name='EDUSEG')
|
||||
|
||||
create_course(
|
||||
course=payload.course,
|
||||
org=org,
|
||||
org=Org(id='*', name='default'),
|
||||
persistence_layer=course_layer,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ LIMIT = 25
|
||||
def me():
|
||||
user: AuthenticatedUser = router.context['user']
|
||||
acls = collect.get_items(
|
||||
KeyPair(user.id, PrefixKey('acls#')),
|
||||
KeyPair(user.id, PrefixKey('acls')),
|
||||
limit=LIMIT,
|
||||
)
|
||||
workspaces = collect.get_items(
|
||||
KeyPair(user.id, PrefixKey('orgs#')),
|
||||
KeyPair(user.id, PrefixKey('orgs')),
|
||||
limit=LIMIT,
|
||||
)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class BadRequestError(MissingError, PowertoolsBadRequestError): ...
|
||||
|
||||
router = Router()
|
||||
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||
collect = DynamoDBCollection(user_layer, exception_cls=BadRequestError)
|
||||
collect = DynamoDBCollection(user_layer, exc_cls=BadRequestError)
|
||||
elastic_client = Elasticsearch(**ELASTIC_CONN)
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ def get_idp(id: str):
|
||||
)
|
||||
def get_emails(id: str):
|
||||
return collect.get_items(
|
||||
KeyPair(id, PrefixKey('emails#')),
|
||||
KeyPair(id, PrefixKey('emails')),
|
||||
start_key=router.current_event.get_query_string_value('start_key', None),
|
||||
)
|
||||
|
||||
@@ -126,6 +126,6 @@ def get_logs(id: str):
|
||||
)
|
||||
def get_orgs(id: str):
|
||||
return collect.get_items(
|
||||
KeyPair(id, PrefixKey('orgs#')),
|
||||
KeyPair(id, PrefixKey('orgs')),
|
||||
start_key=router.current_event.get_query_string_value('start_key', None),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{"id": {"S": "apikey"}, "sk": {"S": "32504457-f133-4c00-936b-6aa712ca9f40"}}
|
||||
{"updateDate": {"S": "2024-02-08T16:42:33.776409-03:00"}, "createDate": {"S": "2019-03-25T00:00:00-03:00"}, "email_verified": {"BOOL": true}, "cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}, "cpf": {"S": "07879819908"}, "sk": {"S": "0"}, "email": {"S": "sergio@somosbeta.com.br"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}, "lastLogin": {"S": "2024-02-08T20:53:45.818126-03:00"}, "orgs": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}, {"S": "edp8njvgQuzNkLx2ySNfAD"}, {"S": "8TVSi5oACLxTiT8ycKPmaQ"}]}}
|
||||
{"sk": {"S": "acl#admin"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "create_date": {"S": "2022-06-13T15:00:24.309410-03:00"}}
|
||||
{"emailVerified": {"BOOL": true}, "updateDate": {"S": "2024-02-08T16:42:33.776409-03:00"}, "createDate": {"S": "2024-01-19T22:53:43.135080-03:00"}, "deliverability": {"S": "DELIVERABLE"}, "sk": {"S": "emails#osergiosiqueira@gmail.com"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "primaryEmail": {"BOOL": false}, "emailDeliverable": {"BOOL": true}}
|
||||
|
||||
@@ -23,7 +23,7 @@ Globals:
|
||||
Architectures:
|
||||
- x86_64
|
||||
Layers:
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:20
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:21
|
||||
Environment:
|
||||
Variables:
|
||||
TZ: America/Sao_Paulo
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{"id": {"S": "apikey"}, "sk": {"S": "32504457-f133-4c00-936b-6aa712ca9f40"}, "tenant": {"M": {"id": {"S": "*"}, "name": {"S": "default"}}}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_verified": {"BOOL": true}, "cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}, "cpf": {"S": "07879819908"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}, "last_login": {"S": "2024-02-08T20:53:45.818126-03:00"}, "tenant:org_id": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}]}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "cognito"}, "create_date": {"S": "2025-03-03T17:12:26.443507-03:00"}, "sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "emails#sergio@somosbeta.com.br"}, "email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_primary": {"BOOL": true}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer
|
||||
|
||||
import auth as app
|
||||
from auth import _parse_bearer_token
|
||||
|
||||
from .conftest import LambdaContext
|
||||
|
||||
|
||||
def test_bearer_jwt(lambda_context: LambdaContext, dynamodb_seeds):
|
||||
def test_bearer_jwt(lambda_context: LambdaContext):
|
||||
# You should mock the Cognito user to pass the test
|
||||
app.get_user = lambda *args, **kwargs: {
|
||||
'sub': '58efed8d-d276-41a8-8502-4ab8b5a6415e',
|
||||
@@ -29,6 +31,31 @@ def test_bearer_jwt(lambda_context: LambdaContext, dynamodb_seeds):
|
||||
}
|
||||
|
||||
|
||||
def test_bearer_apikey(
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
app.collect = DynamoDBCollection(dynamodb_persistence_layer)
|
||||
|
||||
event = {
|
||||
'headers': {
|
||||
'authorization': 'Bearer sk-32504457-f133-4c00-936b-6aa712ca9f40',
|
||||
}
|
||||
}
|
||||
# This data was added from seeds
|
||||
assert app.lambda_handler(event, lambda_context) == {
|
||||
'isAuthorized': True,
|
||||
'context': {'tenant': {'name': 'default', 'id': '*'}},
|
||||
}
|
||||
|
||||
# This data was added from seeds
|
||||
assert app.lambda_handler(
|
||||
{'headers': {'authorization': 'Bearer sk-abc'}},
|
||||
lambda_context,
|
||||
) == {'isAuthorized': False}
|
||||
|
||||
|
||||
def test_parse_bearer_token_api_key():
|
||||
bearer = _parse_bearer_token(
|
||||
'Bearer sk-35433970-6857-4062-bb43-f71683b2f68e',
|
||||
|
||||
2
http-api/uv.lock
generated
2
http-api/uv.lock
generated
@@ -444,7 +444,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "layercake"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = { directory = "../layercake" }
|
||||
dependencies = [
|
||||
{ name = "aws-lambda-powertools", extra = ["all"] },
|
||||
|
||||
Reference in New Issue
Block a user