update session

This commit is contained in:
2025-10-10 12:52:41 -03:00
parent 2de2d4dc0e
commit c9438d49fb
15 changed files with 116 additions and 112 deletions

View File

@@ -106,7 +106,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
pk=enrollment_id, pk=enrollment_id,
sk='0', sk='0',
), ),
update_expr='SET issued_cert = :issued_cert, uploaded_at = :now', update_expr='SET issued_cert = :issued_cert, updated_at = :now',
expr_attr_values={ expr_attr_values={
':now': now_, ':now': now_,
':issued_cert': { ':issued_cert': {

View File

@@ -3,8 +3,20 @@ import os
ISSUER: str = os.getenv('ISSUER') # type: ignore ISSUER: str = os.getenv('ISSUER') # type: ignore
OAUTH2_TABLE: str = os.getenv('OAUTH2_TABLE') # type: ignore OAUTH2_TABLE: str = os.getenv('OAUTH2_TABLE') # type: ignore
OAUTH2_SCOPES_SUPPORTED: str = os.getenv('OAUTH2_SCOPES_SUPPORTED', '')
OAUTH2_REFRESH_TOKEN_EXPIRES_IN = 86_400 * 7 # 7 days OAUTH2_REFRESH_TOKEN_EXPIRES_IN = 86_400 * 7 # 7 days
OAUTH2_SCOPES_SUPPORTED: list[str] = [
'openid',
'profile',
'email',
'offline_access',
'read:users',
'write:users',
'read:enrollments',
'write:enrollments',
'read:orders',
'write:orders',
'read:courses',
'write:courses',
]
SESSION_SECRET: str = os.environ.get('SESSION_SECRET') # type: ignore
SESSION_EXPIRES_IN = 86400 * 30 # 30 days SESSION_EXPIRES_IN = 86400 * 30 # 30 days

View File

@@ -1,4 +1,3 @@
import os
from dataclasses import asdict from dataclasses import asdict
import authlib.oauth2 as oauth2 import authlib.oauth2 as oauth2
@@ -17,24 +16,19 @@ from config import OAUTH2_REFRESH_TOKEN_EXPIRES_IN
from .client import OAuth2Client from .client import OAuth2Client
from .requests import APIGatewayJsonRequest, APIGatewayOAuth2Request from .requests import APIGatewayJsonRequest, APIGatewayOAuth2Request
OAUTH2_SCOPES_SUPPORTED = os.getenv('OAUTH2_SCOPES_SUPPORTED')
logger = Logger(__name__) logger = Logger(__name__)
class AuthorizationServer(oauth2.AuthorizationServer): class AuthorizationServer(oauth2.AuthorizationServer):
def __init__( def __init__(
self, self,
scopes_supported: list[str],
*, *,
persistence_layer: DynamoDBPersistenceLayer, persistence_layer: DynamoDBPersistenceLayer,
) -> None: ) -> None:
super().__init__(scopes_supported=scopes_supported)
self._persistence_layer = persistence_layer self._persistence_layer = persistence_layer
if OAUTH2_SCOPES_SUPPORTED:
super().__init__(
scopes_supported=set(OAUTH2_SCOPES_SUPPORTED.split()),
)
def save_token( def save_token(
self, self,
token: dict, token: dict,

View File

@@ -49,7 +49,7 @@ class APIGatewayOAuth2Request(requests.OAuth2Request):
super().__init__( super().__init__(
request.request_context.http.method, request.request_context.http.method,
uri, uri,
request.headers, headers=request.headers,
) )
self._request = request self._request = request
self.payload = APIGatewayOAuth2Payload(request) self.payload = APIGatewayOAuth2Payload(request)

View File

@@ -3,6 +3,7 @@ from authlib.common.urls import add_params_to_uri
from authlib.jose import JsonWebKey from authlib.jose import JsonWebKey
from authlib.oauth2 import OAuth2Request, rfc7009, rfc9207 from authlib.oauth2 import OAuth2Request, rfc7009, rfc9207
from authlib.oauth2.rfc6749 import ClientMixin, TokenMixin, grants from authlib.oauth2.rfc6749 import ClientMixin, TokenMixin, grants
from authlib.oauth2.rfc6749.hooks import hooked
from authlib.oauth2.rfc6750 import BearerTokenGenerator from authlib.oauth2.rfc6750 import BearerTokenGenerator
from authlib.oauth2.rfc7636 import CodeChallenge from authlib.oauth2.rfc7636 import CodeChallenge
from authlib.oauth2.rfc9068 import JWTBearerTokenGenerator as JWTBearerTokenGenerator_ from authlib.oauth2.rfc9068 import JWTBearerTokenGenerator as JWTBearerTokenGenerator_
@@ -21,7 +22,7 @@ from layercake.dynamodb import (
from layercake.funcs import omit, pick from layercake.funcs import omit, pick
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import ISSUER, OAUTH2_TABLE from config import ISSUER, OAUTH2_SCOPES_SUPPORTED, OAUTH2_TABLE
from integrations.apigateway_oauth2.authorization_server import ( from integrations.apigateway_oauth2.authorization_server import (
AuthorizationServer, AuthorizationServer,
) )
@@ -64,6 +65,8 @@ class OpenIDCode(OpenIDCode_):
} }
def generate_user_info(self, user: User, scope: str) -> UserInfo: def generate_user_info(self, user: User, scope: str) -> UserInfo:
print(scope)
print('--' * 100)
return UserInfo( return UserInfo(
sub=user.id, sub=user.id,
name=user.name, name=user.name,
@@ -173,6 +176,20 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
return User(**pick(('id', 'name', 'email', 'email_verified'), user)) return User(**pick(('id', 'name', 'email', 'email_verified'), user))
class TokenExchangeGrant(grants.BaseGrant):
GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'
TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post']
@hooked
def validate_token_request(self):
raise NotImplementedError()
@hooked
def create_token_response(self):
raise NotImplementedError()
class RefreshTokenNotFoundError(NotFoundError): class RefreshTokenNotFoundError(NotFoundError):
def __init__(self, *_): def __init__(self, *_):
super().__init__('Refresh token not found') super().__init__('Refresh token not found')
@@ -337,7 +354,10 @@ class JWTBearerTokenGenerator(JWTBearerTokenGenerator_):
} }
server = AuthorizationServer(persistence_layer=dyn) server = AuthorizationServer(
persistence_layer=dyn,
scopes_supported=OAUTH2_SCOPES_SUPPORTED,
)
server.register_grant( server.register_grant(
AuthorizationCodeGrant, AuthorizationCodeGrant,
[ [
@@ -353,6 +373,7 @@ server.register_token_generator(
expires_generator=expires_in, expires_generator=expires_in,
), ),
) )
server.register_grant(TokenExchangeGrant)
server.register_grant(RefreshTokenGrant) server.register_grant(RefreshTokenGrant)
server.register_endpoint(RevocationEndpoint) server.register_endpoint(RevocationEndpoint)
server.register_extension(IssuerParameter()) server.register_extension(IssuerParameter())

View File

@@ -1,6 +1,5 @@
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
import jwt
from authlib.oauth2.rfc6749 import errors from authlib.oauth2.rfc6749 import errors
from authlib.oauth2.rfc6749.util import scope_to_list from authlib.oauth2.rfc6749.util import scope_to_list
from aws_lambda_powertools import Logger from aws_lambda_powertools import Logger
@@ -9,12 +8,12 @@ from aws_lambda_powertools.event_handler.exceptions import (
BadRequestError, BadRequestError,
ForbiddenError, ForbiddenError,
ServiceError, ServiceError,
UnauthorizedError,
) )
from joserfc.errors import JoseError
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import ISSUER, OAUTH2_TABLE, SESSION_SECRET from config import OAUTH2_TABLE
from oauth2 import server from oauth2 import server
router = Router() router = Router()
@@ -26,19 +25,24 @@ dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
def authorize(): def authorize():
current_event = router.current_event current_event = router.current_event
cookies = _parse_cookies(current_event.get('cookies', [])) cookies = _parse_cookies(current_event.get('cookies', []))
session_id = cookies.get('session_id') session = cookies.get('__session')
if not session_id: if not session:
raise BadRequestError('Missing session_id') raise BadRequestError('Missing session')
try: try:
sub, session_scope = verify_session(session_id) sid, sub = session.split(':')
# Raise if session is not found
dyn.collection.get_item(
KeyPair('SESSION', sid),
exc_cls=InvalidSession,
)
grant = server.get_consent_grant( grant = server.get_consent_grant(
request=router.current_event, request=router.current_event,
end_user=sub, end_user=sub,
) )
user_scopes = _user_scopes(sub)
client_scopes = set(scope_to_list(grant.client.scope)) client_scopes = set(scope_to_list(grant.client.scope))
user_scopes = set(scope_to_list(session_scope)) if session_scope else set()
# Deny authorization if user lacks scopes requested by client # Deny authorization if user lacks scopes requested by client
if not client_scopes.issubset(user_scopes): if not client_scopes.issubset(user_scopes):
@@ -49,7 +53,7 @@ def authorize():
grant_user=sub, grant_user=sub,
grant=grant, grant=grant,
) )
except jwt.exceptions.InvalidTokenError as err: except JoseError as err:
logger.exception(err) logger.exception(err)
raise BadRequestError(str(err)) raise BadRequestError(str(err))
except errors.OAuth2Error as err: except errors.OAuth2Error as err:
@@ -60,39 +64,19 @@ def authorize():
) )
def verify_session(session_id: str) -> tuple[str, str | None]: def _user_scopes(sub: str) -> set:
payload = jwt.decode( return set(
session_id, scope_to_list(
SESSION_SECRET, dyn.collection.get_item(
algorithms=['HS256'], KeyPair(
issuer=ISSUER, pk=sub,
options={ sk=SortKey(sk='SCOPE', path_spec='scope'),
'require': ['exp', 'sub', 'iss', 'sid'], ),
}, exc_cls=BadRequestError,
) )
user = dyn.collection.get_items(
KeyPair(
pk='SESSION',
sk=payload['sid'],
rename_key='session',
) )
+ KeyPair(
pk=payload['sub'],
sk=SortKey(
sk='SCOPE',
path_spec='scope',
rename_key='scope',
),
),
flatten_top=False,
) )
if 'session' not in user:
raise SessionRevokedError('Session revoked')
return payload['sub'], user.get('scope')
def _parse_cookies(cookies: list[str] | None) -> dict[str, str]: def _parse_cookies(cookies: list[str] | None) -> dict[str, str]:
parsed_cookies = {} parsed_cookies = {}
@@ -108,4 +92,4 @@ def _parse_cookies(cookies: list[str] | None) -> dict[str, str]:
return parsed_cookies return parsed_cookies
class SessionRevokedError(UnauthorizedError): ... class InvalidSession(BadRequestError): ...

View File

@@ -3,7 +3,6 @@ from typing import Annotated
from uuid import uuid4 from uuid import uuid4
import boto3 import boto3
import jwt
from aws_lambda_powertools.event_handler import ( from aws_lambda_powertools.event_handler import (
Response, Response,
) )
@@ -17,10 +16,8 @@ from passlib.hash import pbkdf2_sha256
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import ( from config import (
ISSUER,
OAUTH2_TABLE, OAUTH2_TABLE,
SESSION_EXPIRES_IN, SESSION_EXPIRES_IN,
SESSION_SECRET,
) )
router = Router() router = Router()
@@ -45,7 +42,7 @@ def session(
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
cookies=[ cookies=[
Cookie( Cookie(
name='session_id', name='__session',
value=new_session(user_id), value=new_session(user_id),
http_only=True, http_only=True,
secure=True, secure=True,
@@ -127,26 +124,15 @@ def _get_idp_user(
def new_session(sub: str) -> str: def new_session(sub: str) -> str:
session_id = str(uuid4()) sid = str(uuid4())
now_ = now() now_ = now()
exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN) exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN)
token = jwt.encode(
{
'sid': session_id,
'sub': sub,
'iss': ISSUER,
'iat': int(now_.timestamp()),
'exp': exp,
},
SESSION_SECRET,
algorithm='HS256',
)
with dyn.transact_writer() as transact: with dyn.transact_writer() as transact:
transact.put( transact.put(
item={ item={
'id': 'SESSION', 'id': 'SESSION',
'sk': session_id, 'sk': sid,
'user_id': sub, 'user_id': sub,
'ttl': exp, 'ttl': exp,
'created_at': now_, 'created_at': now_,
@@ -155,13 +141,13 @@ def new_session(sub: str) -> str:
transact.put( transact.put(
item={ item={
'id': sub, 'id': sub,
'sk': f'SESSION#{session_id}', 'sk': f'SESSION#{sid}',
'ttl': exp, 'ttl': exp,
'created_at': now_, 'created_at': now_,
} }
) )
return token return f'{sid}:{sub}'
class UserNotFoundError(NotFoundError): class UserNotFoundError(NotFoundError):

View File

@@ -11,7 +11,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
issuerUrl.search = url.search issuerUrl.search = url.search
redirect.search = url.search redirect.search = url.search
if (!cookies.session_id) { if (!cookies?.__session) {
return new Response(null, { return new Response(null, {
status: httpStatus.FOUND, status: httpStatus.FOUND,
headers: { headers: {

View File

@@ -14,7 +14,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:96 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:98
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo
@@ -25,8 +25,6 @@ Globals:
DYNAMODB_SORT_KEY: sk DYNAMODB_SORT_KEY: sk
OAUTH2_TABLE: !Ref OAuth2Table OAUTH2_TABLE: !Ref OAuth2Table
ISSUER: https://id.saladeaula.digital ISSUER: https://id.saladeaula.digital
SESSION_SECRET: 7DUTFB1iLeSpiXvmxbOZim1yPVmQbmBpAzgscob0RDzrL2wVwRi1ti2ZSry7jJAf
OAUTH2_SCOPES_SUPPORTED: openid profile email offline_access read:users read:enrollments read:orders read:courses write:courses
Resources: Resources:
HttpLog: HttpLog:

View File

@@ -21,9 +21,6 @@ def pytest_configure():
os.environ['DYNAMODB_PARTITION_KEY'] = PK os.environ['DYNAMODB_PARTITION_KEY'] = PK
os.environ['DYNAMODB_SORT_KEY'] = SK os.environ['DYNAMODB_SORT_KEY'] = SK
os.environ['ISSUER'] = 'http://localhost' os.environ['ISSUER'] = 'http://localhost'
os.environ['OAUTH2_SCOPES_SUPPORTED'] = (
'openid profile email offline_access read:users'
)
@dataclass @dataclass

View File

@@ -16,7 +16,7 @@ def test_authorize(
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
session_id = new_session(USER_ID) session = new_session(USER_ID)
r = app.lambda_handler( r = app.lambda_handler(
http_api_proxy( http_api_proxy(
@@ -31,7 +31,7 @@ def test_authorize(
'state': '456', 'state': '456',
}, },
cookies=[ cookies=[
f'session_id={session_id}; HttpOnly; Secure', f'__session={session}; HttpOnly; Secure',
], ],
), ),
lambda_context, lambda_context,
@@ -60,7 +60,7 @@ def test_forbidden(
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
session_id = new_session('fd5914ec-fd37-458b-b6b9-8aeab38b666b') session = new_session('fd5914ec-fd37-458b-b6b9-8aeab38b666b')
r = app.lambda_handler( r = app.lambda_handler(
http_api_proxy( http_api_proxy(
@@ -75,7 +75,7 @@ def test_forbidden(
'state': '456', 'state': '456',
}, },
cookies=[ cookies=[
f'session_id={session_id}; HttpOnly; Secure', f'__session={session}; HttpOnly; Secure',
], ],
), ),
lambda_context, lambda_context,
@@ -84,15 +84,13 @@ def test_forbidden(
assert r['statusCode'] == HTTPStatus.FORBIDDEN assert r['statusCode'] == HTTPStatus.FORBIDDEN
def test_authorize_revoked( def test_invalid_session(
app, app,
seeds, seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer, dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
invalid_session_id = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiIwNTgzNTBhYi02NGU1LTQ0MzEtYmQyNy01MGVhOWIxNmQxZGYiLCJzdWIiOiIzNTdkYjFjNS03NDQyLTQwNzUtOThhMy1mYmU1YzkzOGE0MTkiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwiaWF0IjoxNzU1Mzk3Nzk5LCJleHAiOjE3NTUzOTg2OTl9.dDbiHYReVERbkNH2df4sXK2VIwT7G1KjNC5UrBuN6IQ'
r = app.lambda_handler( r = app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/authorize', raw_path='/authorize',
@@ -106,7 +104,7 @@ def test_authorize_revoked(
'state': '456', 'state': '456',
}, },
cookies=[ cookies=[
f'session_id={invalid_session_id}; HttpOnly; Secure', '__session=10:10; HttpOnly; Secure',
], ],
), ),
lambda_context, lambda_context,

View File

@@ -16,9 +16,9 @@
// User data // User data
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br"} {"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br"}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "PASSWORD", "hash": "$pbkdf2-sha256$29000$IuTcm7M2BiAEgPB.b.3dGw$d8xVCbx8zxg7MeQBrOvCOgniiilsIHEMHzoH/OXftLQ"} {"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "PASSWORD", "hash": "$pbkdf2-sha256$29000$IuTcm7M2BiAEgPB.b.3dGw$d8xVCbx8zxg7MeQBrOvCOgniiilsIHEMHzoH/OXftLQ"}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SCOPE", "scope": "openid profile email offline_access read:users read:courses"} {"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SCOPE", "scope": ["openid", "profile", "email", "offline_access", "read:users", "read:courses", "impersonate:users"]}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SESSION#36af142e-9f6d-49d3-bfe9-6a6bd6ab2712", "created_at": "2025-09-17T13:44:34.544491-03:00", "ttl": 1760719474} {"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SESSION#36af142e-9f6d-49d3-bfe9-6a6bd6ab2712", "created_at": "2025-09-17T13:44:34.544491-03:00", "ttl": 1760719474}
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "0", "name": "Johnny Cash", "email": "johnny@johnnycash.com"} {"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "0", "name": "Johnny Cash", "email": "johnny@johnnycash.com"}
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "PASSWORD", "hash": "$pbkdf2-sha256$29000$IuTcm7M2BiAEgPB.b.3dGw$d8xVCbx8zxg7MeQBrOvCOgniiilsIHEMHzoH/OXftLQ"} {"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "PASSWORD", "hash": "$pbkdf2-sha256$29000$IuTcm7M2BiAEgPB.b.3dGw$d8xVCbx8zxg7MeQBrOvCOgniiilsIHEMHzoH/OXftLQ"}
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "SCOPE", "scope": "openid"} {"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "SCOPE", "scope": ["openid"]}

View File

@@ -35,14 +35,14 @@ wheels = [
[[package]] [[package]]
name = "authlib" name = "authlib"
version = "1.6.1" version = "1.6.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cryptography" }, { name = "cryptography" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988, upload-time = "2025-07-20T07:38:42.834Z" } sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299, upload-time = "2025-07-20T07:38:39.259Z" }, { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
] ]
[[package]] [[package]]
@@ -469,6 +469,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
] ]
[[package]]
name = "joserfc"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/26/a0/4b8dfecc8ec3c15aa1f2ff7d5b947344378b5b595ce37c8a8fe6e25c1400/joserfc-1.4.0.tar.gz", hash = "sha256:e8c2f327bf10a937d284d57e9f8aec385381e5e5850469b50a7dade1aba59759", size = 196339, upload-time = "2025-10-09T07:47:00.835Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/05/342459b7629c6fcb5f99a646886ee2904491955b8cce6b26b0b9a498f67c/joserfc-1.4.0-py3-none-any.whl", hash = "sha256:46917e6b53f1ec0c7e20d34d6f3e6c27da0fa43d0d4ebfb89aada7c86582933a", size = 66390, upload-time = "2025-10-09T07:46:59.591Z" },
]
[[package]] [[package]]
name = "jsonlines" name = "jsonlines"
version = "4.0.0" version = "4.0.0"
@@ -495,7 +507,7 @@ wheels = [
[[package]] [[package]]
name = "layercake" name = "layercake"
version = "0.9.14" version = "0.10.1"
source = { directory = "../layercake" } source = { directory = "../layercake" }
dependencies = [ dependencies = [
{ name = "arnparse" }, { name = "arnparse" },
@@ -504,6 +516,7 @@ dependencies = [
{ name = "dictdiffer" }, { name = "dictdiffer" },
{ name = "ftfy" }, { name = "ftfy" },
{ name = "glom" }, { name = "glom" },
{ name = "joserfc" },
{ name = "meilisearch" }, { name = "meilisearch" },
{ name = "orjson" }, { name = "orjson" },
{ name = "passlib" }, { name = "passlib" },
@@ -511,7 +524,7 @@ dependencies = [
{ name = "pycpfcnpj" }, { name = "pycpfcnpj" },
{ name = "pydantic", extra = ["email"] }, { name = "pydantic", extra = ["email"] },
{ name = "pydantic-extra-types" }, { name = "pydantic-extra-types" },
{ name = "pyjwt" }, { name = "python-multipart" },
{ name = "pytz" }, { name = "pytz" },
{ name = "requests" }, { name = "requests" },
{ name = "smart-open", extra = ["s3"] }, { name = "smart-open", extra = ["s3"] },
@@ -522,11 +535,12 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "arnparse", specifier = ">=0.0.2" }, { name = "arnparse", specifier = ">=0.0.2" },
{ name = "authlib", specifier = ">=1.6.1" }, { name = "authlib", specifier = ">=1.6.5" },
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.18.0" }, { name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.18.0" },
{ name = "dictdiffer", specifier = ">=0.9.0" }, { name = "dictdiffer", specifier = ">=0.9.0" },
{ name = "ftfy", specifier = ">=6.3.1" }, { name = "ftfy", specifier = ">=6.3.1" },
{ name = "glom", specifier = ">=24.11.0" }, { name = "glom", specifier = ">=24.11.0" },
{ name = "joserfc", specifier = ">=1.2.2" },
{ name = "meilisearch", specifier = ">=0.34.0" }, { name = "meilisearch", specifier = ">=0.34.0" },
{ name = "orjson", specifier = ">=3.10.15" }, { name = "orjson", specifier = ">=3.10.15" },
{ name = "passlib", specifier = ">=1.7.4" }, { name = "passlib", specifier = ">=1.7.4" },
@@ -534,7 +548,7 @@ requires-dist = [
{ name = "pycpfcnpj", specifier = ">=1.8" }, { name = "pycpfcnpj", specifier = ">=1.8" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" }, { name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
{ name = "pydantic-extra-types", specifier = ">=2.10.3" }, { name = "pydantic-extra-types", specifier = ">=2.10.3" },
{ name = "pyjwt", specifier = ">=2.10.1" }, { name = "python-multipart", specifier = ">=0.0.20" },
{ name = "pytz", specifier = ">=2025.1" }, { name = "pytz", specifier = ">=2025.1" },
{ name = "requests", specifier = ">=2.32.3" }, { name = "requests", specifier = ">=2.32.3" },
{ name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" }, { name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" },
@@ -825,15 +839,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
] ]
[[package]]
name = "pyjwt"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.4.1" version = "8.4.1"
@@ -885,6 +890,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
] ]
[[package]]
name = "python-multipart"
version = "0.0.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
]
[[package]] [[package]]
name = "pytz" name = "pytz"
version = "2025.2" version = "2025.2"

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "layercake" name = "layercake"
version = "0.10.0" version = "0.10.1"
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions." description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
readme = "README.md" readme = "README.md"
authors = [ authors = [
@@ -23,11 +23,11 @@ dependencies = [
"sqlite-utils>=3.38", "sqlite-utils>=3.38",
"dictdiffer>=0.9.0", "dictdiffer>=0.9.0",
"unidecode>=1.4.0", "unidecode>=1.4.0",
"authlib>=1.6.1",
"passlib>=1.7.4", "passlib>=1.7.4",
"psycopg[binary]>=3.2.9", "psycopg[binary]>=3.2.9",
"joserfc>=1.2.2", "joserfc>=1.2.2",
"python-multipart>=0.0.20", "python-multipart>=0.0.20",
"authlib>=1.6.5",
] ]
[dependency-groups] [dependency-groups]

8
layercake/uv.lock generated
View File

@@ -44,14 +44,14 @@ wheels = [
[[package]] [[package]]
name = "authlib" name = "authlib"
version = "1.6.1" version = "1.6.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cryptography" }, { name = "cryptography" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988, upload-time = "2025-07-20T07:38:42.834Z" } sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299, upload-time = "2025-07-20T07:38:39.259Z" }, { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
] ]
[[package]] [[package]]
@@ -714,7 +714,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "arnparse", specifier = ">=0.0.2" }, { name = "arnparse", specifier = ">=0.0.2" },
{ name = "authlib", specifier = ">=1.6.1" }, { name = "authlib", specifier = ">=1.6.5" },
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.18.0" }, { name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.18.0" },
{ name = "dictdiffer", specifier = ">=0.9.0" }, { name = "dictdiffer", specifier = ">=0.9.0" },
{ name = "ftfy", specifier = ">=6.3.1" }, { name = "ftfy", specifier = ">=6.3.1" },