add reset password

This commit is contained in:
2025-04-14 10:49:28 -03:00
parent 273c580139
commit e472826dcc
17 changed files with 228 additions and 56 deletions

View File

@@ -12,7 +12,7 @@ from aws_lambda_powertools.event_handler.exceptions import ServiceError
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext
from middlewares import AuthorizerMiddleware
from middlewares import AuthenticationMiddleware
from routes import courses, enrollments, lookup, orders, orgs, settings, users, webhooks
tracer = Tracer()
@@ -28,7 +28,7 @@ app = APIGatewayHttpResolver(
cors=cors,
debug='AWS_SAM_LOCAL' in os.environ,
)
app.use(middlewares=[AuthorizerMiddleware()])
app.use(middlewares=[AuthenticationMiddleware()])
app.include_router(courses.router, prefix='/courses')
app.include_router(enrollments.router, prefix='/enrollments')
app.include_router(orders.router, prefix='/orders')

View File

@@ -36,3 +36,40 @@ def admin_get_user(
return None
else:
return user
def admin_set_user_password(
username: str,
password: str,
*,
user_pool_id: str,
permanent: bool = False,
idp_client,
) -> bool:
"""Sets the specified user's password in a user pool as an administrator.
Works on any user.
The password can be temporary or permanent. If it is temporary, the user
status enters the FORCE_CHANGE_PASSWORD state.
When the user next tries to sign in, the InitiateAuth/AdminInitiateAuth
response will contain the NEW_PASSWORD_REQUIRED challenge.
If the user doesn't sign in before it expires, the user won't be able
to sign in, and an administrator must reset their password.
Once the user has set a new password, or the password is permanent,
the user status is set to Confirmed.
"""
try:
idp_client.admin_set_user_password(
UserPoolId=user_pool_id,
Username=username,
Password=password,
Permanent=permanent,
)
except idp_client.exceptions as err:
logger.exception(err)
return False
else:
return True

View File

@@ -1,9 +1,9 @@
from .audit_log_middleware import AuditLogMiddleware
from .authorizer_middleware import AuthorizerMiddleware, User
from .authentication_middleware import AuthenticationMiddleware, User
from .tenant_middelware import Tenant, TenantMiddleware
__all__ = [
'AuthorizerMiddleware',
'AuthenticationMiddleware',
'AuditLogMiddleware',
'TenantMiddleware',
'User',

View File

@@ -17,7 +17,7 @@ from layercake.dynamodb import (
)
from layercake.funcs import pick
from .authorizer_middleware import User
from .authentication_middleware import User
YEAR_DAYS = 365
LOG_RETENTION_DAYS = YEAR_DAYS * 2
@@ -29,14 +29,14 @@ class AuditLogMiddleware(BaseMiddlewareHandler):
Parameters
----------
action : str
action: str
The identifier for the audit log action.
collect : DynamoDBCollection
collect: DynamoDBCollection
The collection instance used to persist the audit log data.
audit_attrs : tuple of str, optional
audit_attrs: tuple of str, optional
A tuple of attribute names to extract from the response body for logging.
These represent the specific fields to include in the audit log.
retention_days : int or None, optional
retention_days: int or None, optional
The number of days the log is retained on the server.
If None, no time-to-live (TTL) will be applied.
"""
@@ -64,10 +64,13 @@ class AuditLogMiddleware(BaseMiddlewareHandler):
ip_addr = req_context.http.source_ip
response = next_middleware(app)
print(app.context['_route'])
# Successful response
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status#successful_responses
if 200 <= response.status_code < 300 and user:
now_ = now()
author = pick(('id', 'name'), dict(user))
data = (
pick(self.audit_attrs, extract_event_from_common_models(response.body))
if response.is_json()
@@ -89,7 +92,7 @@ class AuditLogMiddleware(BaseMiddlewareHandler):
action=self.action,
data=data,
ip=ip_addr,
author='himself',
author=author,
ttl=retention_days,
)

View File

@@ -23,7 +23,7 @@ class CognitoUser(User):
sub: UUID4
class AuthorizerMiddleware(BaseMiddlewareHandler):
class AuthenticationMiddleware(BaseMiddlewareHandler):
"""This middleware extracts user authentication details from the Lambda authorizer context
and makes them available in the application context."""

View File

@@ -18,7 +18,7 @@ from pydantic import UUID4, BaseModel
from auth import AuthFlowType
from .authorizer_middleware import User
from .authentication_middleware import User
class Tenant(BaseModel):

View File

@@ -15,7 +15,7 @@ import elastic
from boto3clients import dynamodb_client
from enrollment import set_status_as_canceled
from middlewares.audit_log_middleware import AuditLogMiddleware
from middlewares.authorizer_middleware import User
from middlewares.authentication_middleware import User
from settings import ELASTIC_CONN, ENROLLMENT_TABLE, USER_TABLE
router = Router()

View File

@@ -24,6 +24,7 @@ from pydantic import UUID4, BaseModel, EmailStr, StringConstraints
import cognito
import elastic
import middlewares
from boto3clients import dynamodb_client, idp_client
from middlewares import AuditLogMiddleware
from models import User
@@ -70,9 +71,31 @@ class Password(BaseModel):
new_password: Annotated[str, StringConstraints(min_length=6)]
@router.post('/<id>/password', compress=True, tags=['User'], include_in_schema=False)
def new_password(id: str, payload: Password):
return Response(status_code=HTTPStatus.OK)
@router.post(
'/<id>/password',
compress=True,
tags=['User'],
include_in_schema=False,
middlewares=[
AuditLogMiddleware('PASSWORD_RESET', user_collect, ('id', 'cognito_sub'))
],
)
def password(id: str, payload: Password):
cognito.admin_set_user_password(
username=str(payload.cognito_sub),
password=payload.new_password,
user_pool_id=USER_POOOL_ID,
idp_client=idp_client,
)
return Response(
body={
'id': id,
'cognito_sub': payload.cognito_sub,
},
content_type=content_types.APPLICATION_JSON,
status_code=HTTPStatus.OK,
)
@router.get('/<id>', compress=True, tags=['User'], summary='Get user')
@@ -80,10 +103,10 @@ def get_user(id: str):
return user_collect.get_item(KeyPair(id, '0'))
@router.get('/<id>/idp', compress=True, include_in_schema=False)
def get_idp(id: str):
@router.get('/<sub>/idp', compress=True, include_in_schema=False)
def get_idp(sub: str):
return cognito.admin_get_user(
sub=id,
sub=sub,
user_pool_id=USER_POOOL_ID,
idp_client=idp_client,
)

View File

@@ -14,7 +14,7 @@ from ..conftest import HttpApiProxy, LambdaContext
YEAR_DAYS = 365
def test_get_course(
def test_get_courses(
mock_app,
dynamodb_seeds,
http_api_proxy: HttpApiProxy,

View File

@@ -1,11 +1,7 @@
import json
from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import (
DynamoDBCollection,
DynamoDBPersistenceLayer,
KeyPair,
)
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer, KeyPair
from ..conftest import HttpApiProxy, LambdaContext
@@ -32,7 +28,7 @@ def test_get_policies(
}
def test_put_org(
def test_put_policies(
mock_app,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
@@ -44,7 +40,7 @@ def test_put_org(
raw_path='/orgs/cJtK9SsnJhKPyxESe7g3DG/policies',
method=HTTPMethod.PUT,
headers={'X-Tenant': '*'},
body={},
body={'payment_policy': None},
),
lambda_context,
)

View File

@@ -31,7 +31,16 @@ def test_get_emails(
'id': '5OxmMjL-ujoR5IMGegQz',
'create_date': '2019-03-25T00:00:00-03:00',
'update_date': '2023-11-09T12:13:04.308986-03:00',
}
},
{
'email_verified': True,
'mx_record_exists': True,
'sk': 'osergiosiqueira@gmail.com',
'email_primary': False,
'id': '5OxmMjL-ujoR5IMGegQz',
'create_date': '2019-03-25T00:00:00-03:00',
'update_date': '2023-11-09T12:13:04.308986-03:00',
},
],
'last_key': None,
}

View File

@@ -4,7 +4,7 @@ import pytest
from aws_lambda_powertools.event_handler.api_gateway import APIGatewayHttpResolver
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer
from middlewares import AuthorizerMiddleware, TenantMiddleware
from middlewares import AuthenticationMiddleware, TenantMiddleware
from .conftest import HttpApiProxy, LambdaContext
@@ -13,7 +13,7 @@ from .conftest import HttpApiProxy, LambdaContext
def mock_app(dynamodb_persistence_layer: DynamoDBPersistenceLayer):
collect = DynamoDBCollection(dynamodb_persistence_layer)
app = APIGatewayHttpResolver()
app.use(middlewares=[AuthorizerMiddleware(), TenantMiddleware(collect)])
app.use(middlewares=[AuthenticationMiddleware(), TenantMiddleware(collect)])
@app.get('/')
def index():

View File

@@ -67,7 +67,7 @@ def del_email(
key=KeyPair('email', email),
)
transact.delete(
key=KeyPair(id, ComposeKey(email, 'emails')),
key=KeyPair(id, ComposeKey(email, prefix='emails')),
cond_expr='email_primary <> :primary',
expr_attr_values={':primary': True},
)