diff --git a/api.saladeaula.digital/app/routes/orgs/users/__init__.py b/api.saladeaula.digital/app/routes/orgs/users/__init__.py index dea4cba..9f8be88 100644 --- a/api.saladeaula.digital/app/routes/orgs/users/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/users/__init__.py @@ -121,6 +121,8 @@ def _create_user(user: User, org: Org) -> bool: # 'org_id': {org.id}, # 'created_at': now_, 'createDate': now_, + # Makes the email searchable + 'emails': {user.email}, }, ) transact.put( @@ -137,8 +139,8 @@ def _create_user(user: User, org: Org) -> bool: 'sk': f'emails#{user.email}', 'email_verified': email_verified, 'email_primary': True, - 'mx_record_exists': email_verified, 'created_at': now_, + **({'mx_record_exists': True} if email_verified else {}), } ) diff --git a/api.saladeaula.digital/app/routes/users/emails.py b/api.saladeaula.digital/app/routes/users/emails.py index 0302600..17a17b5 100644 --- a/api.saladeaula.digital/app/routes/users/emails.py +++ b/api.saladeaula.digital/app/routes/users/emails.py @@ -20,13 +20,9 @@ router = Router() dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -@router.get('//emails') -def get_emails(user_id: str, start_key: Annotated[str | None, Query] = None): - return dyn.collection.query( - # Post-migration (users): rename `emails` to `EMAIL` - key=KeyPair(user_id, 'emails'), - start_key=start_key, - ) +class ConflictError(ServiceError): + def __init__(self, msg: str | dict): + super().__init__(HTTPStatus.CONFLICT, msg) class UserNotFoundError(NotFoundError): ... @@ -35,9 +31,19 @@ class UserNotFoundError(NotFoundError): ... class EmailNotFoundError(NotFoundError): ... -class EmailConflictError(ServiceError): - def __init__(self, msg: str | dict): - super().__init__(HTTPStatus.CONFLICT, msg) +class EmailVerificationNotFoundError(NotFoundError): ... + + +class EmailConflictError(ConflictError): ... + + +@router.get('//emails') +def get_emails(user_id: str, start_key: Annotated[str | None, Query] = None): + return dyn.collection.query( + # Post-migration (users): rename `emails` to `EMAIL` + key=KeyPair(user_id, 'emails'), + start_key=start_key, + ) @router.post('//emails') @@ -68,7 +74,6 @@ def add( # Post-migration (users): rename `emails` to `EMAIL` 'sk': f'emails#{email}', 'email_verified': False, - 'mx_record_exists': False, 'email_primary': False, 'created_at': now_, } @@ -124,9 +129,6 @@ def request_verification( return JSONResponse(status_code=HTTPStatus.NO_CONTENT) -class EmailVerificationNotFoundError(NotFoundError): ... - - @router.post('//emails//verify') def verify(user_id: str, code: str): r = dyn.collection.get_items( diff --git a/apps/id.saladeaula.digital/app/routes/authorize.ts b/apps/id.saladeaula.digital/app/routes/authorize.ts index 0cbf627..c21dbb6 100644 --- a/apps/id.saladeaula.digital/app/routes/authorize.ts +++ b/apps/id.saladeaula.digital/app/routes/authorize.ts @@ -20,6 +20,10 @@ export async function loader({ request, context }: Route.LoaderArgs) { }) } + if (!url.searchParams.has('client_id')) { + throw redirect('https://scorm.eduseg.workers.dev/') + } + const r = await fetch(issuerUrl.toString(), { method: 'GET', headers: new Headers([ diff --git a/id.saladeaula.digital/app/routes/authentication.py b/id.saladeaula.digital/app/routes/authentication.py index f142744..3cbde52 100644 --- a/id.saladeaula.digital/app/routes/authentication.py +++ b/id.saladeaula.digital/app/routes/authentication.py @@ -7,7 +7,10 @@ from aws_lambda_powertools.event_handler import ( Response, ) from aws_lambda_powertools.event_handler.api_gateway import Router -from aws_lambda_powertools.event_handler.exceptions import ForbiddenError, NotFoundError +from aws_lambda_powertools.event_handler.exceptions import ( + NotFoundError, + UnauthorizedError, +) from aws_lambda_powertools.event_handler.openapi.params import Body from aws_lambda_powertools.shared.cookies import Cookie from layercake.dateutils import now, ttl @@ -25,7 +28,7 @@ dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client) idp = boto3.client('cognito-idp') -class InvalidCredentialsError(ForbiddenError): ... +class InvalidCredentialsError(UnauthorizedError): ... class UserNotFoundError(NotFoundError): ... @@ -42,15 +45,21 @@ def authentication( _get_idp_user(user_id, username, password) else: if not pbkdf2_sha256.verify(password, password_hash): - raise InvalidCredentialsError('Invalid credentials') + dyn.update_item( + key=KeyPair(user_id, 'FAILED_ATTEMPTS'), + update_expr='SET #count = if_not_exists(#count, :zero) + :one, \ + updated_at = :now', + expr_attr_names={ + '#count': 'failed_attempts', + }, + expr_attr_values={ + ':zero': 0, + ':one': 1, + ':now': now(), + }, + ) - dyn.update_item( - key=KeyPair(user_id, '0'), - # Post-migration (users): uncomment the following line - # update_expr='SET last_login = :now', - update_expr='SET lastLogin = :now', - expr_attr_values={':now': now()}, - ) + raise InvalidCredentialsError('Invalid credentials') return Response( status_code=HTTPStatus.OK, @@ -146,6 +155,16 @@ def new_session(user_id: str) -> str: exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN) with dyn.transact_writer() as transact: + transact.delete(key=KeyPair(user_id, 'FAILED_ATTEMPTS')) + transact.update( + key=KeyPair(user_id, '0'), + # Post-migration (users): uncomment the following line + # update_expr='SET last_login = :now', + update_expr='SET lastLogin = :now', + expr_attr_values={ + ':now': now_, + }, + ) transact.put( item={ 'id': 'SESSION', diff --git a/id.saladeaula.digital/tests/routes/test_authentication.py b/id.saladeaula.digital/tests/routes/test_authentication.py index ce8e3fa..7cf8d79 100644 --- a/id.saladeaula.digital/tests/routes/test_authentication.py +++ b/id.saladeaula.digital/tests/routes/test_authentication.py @@ -1,6 +1,6 @@ -from http import HTTPMethod +from http import HTTPMethod, HTTPStatus -from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey from ..conftest import HttpApiProxy, LambdaContext @@ -29,3 +29,31 @@ def test_authentication( session = dynamodb_persistence_layer.collection.query(PartitionKey('SESSION')) # One seesion if created from seeds assert len(session['items']) == 2 + + +def test_invalid_password( + app, + seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = app.lambda_handler( + http_api_proxy( + raw_path='/authentication', + method=HTTPMethod.POST, + body={ + 'username': '07879819908', + 'password': '123333', + }, + ), + lambda_context, + ) + + assert r['statusCode'] == HTTPStatus.UNAUTHORIZED + + failed = dynamodb_persistence_layer.collection.get_item( + KeyPair('357db1c5-7442-4075-98a3-fbe5c938a419', 'FAILED_ATTEMPTS') + ) + + assert 'failed_attempts' in failed diff --git a/konviva-events/template.yaml b/konviva-events/template.yaml index 2ff1588..71e7b8d 100644 --- a/konviva-events/template.yaml +++ b/konviva-events/template.yaml @@ -32,7 +32,7 @@ Globals: ENROLLMENT_TABLE: !Ref EnrollmentTable COURSE_TABLE: !Ref CourseTable KONVIVA_API_URL: https://lms.saladeaula.digital - KONVIVA_SECRET_KEY: "{{resolve:ssm:/betaeducacao/konviva/secret_key/str}}" + KONVIVA_SECRET_KEY: '{{resolve:ssm:/betaeducacao/konviva/secret_key/str}}' Resources: EventLog: @@ -49,7 +49,7 @@ Resources: Type: AWS::Serverless::HttpApi Properties: CorsConfiguration: - AllowOrigins: ["*"] + AllowOrigins: ['*'] AllowMethods: [POST, OPTIONS] AllowHeaders: [Content-Type, X-Requested-With] @@ -90,12 +90,34 @@ Resources: detail-type: [INSERT] detail: new_image: - sk: ["0"] + sk: ['0'] cnpj: - exists: false metadata__konviva_user_id: - exists: false + EventCreateOrgFunction: + Type: AWS::Serverless::Function + Properties: + Handler: events.create_org.lambda_handler + LoggingConfig: + LogGroup: !Ref EventLog + Policies: + - DynamoDBWritePolicy: + TableName: !Ref UserTable + Events: + DynamoDBEvent: + Type: EventBridgeRule + Properties: + Pattern: + resources: [!Ref UserTable] + detail-type: [INSERT] + detail: + new_image: + sk: ['0'] + cnpj: + - exists: true + EventUpdateUserFunction: Type: AWS::Serverless::Function Properties: @@ -114,7 +136,7 @@ Resources: detail-type: [MODIFY] detail: new_image: - sk: ["0"] + sk: ['0'] metadata__konviva_user_id: - exists: true changes: [name, email, cpf] @@ -159,7 +181,7 @@ Resources: detail-type: [MODIFY] detail: new_image: - sk: ["0"] + sk: ['0'] status: [CANCELED] old_image: status: [PENDING] diff --git a/users-events/app/events/stopgap/add_tenant.py b/users-events/app/events/stopgap/add_tenant.py index 49f2486..bbfc961 100644 --- a/users-events/app/events/stopgap/add_tenant.py +++ b/users-events/app/events/stopgap/add_tenant.py @@ -20,7 +20,8 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: user_layer.update_item( key=KeyPair(new_image['sk'], '0'), - update_expr='ADD tenant_id :tenant_id SET updated_at = :updated_at', + update_expr='ADD tenant_id :tenant_id \ + SET updated_at = :updated_at', expr_attr_values={ ':tenant_id': {tenant}, ':updated_at': now_, diff --git a/users-events/template.yaml b/users-events/template.yaml index 19beb0b..8dd716f 100644 --- a/users-events/template.yaml +++ b/users-events/template.yaml @@ -242,6 +242,7 @@ Resources: detail: new_image: sk: + # Post-migration (users): rename `emails` to `EMAIL` - prefix: emails# mx_record_exists: - exists: false