Skip to content

Commit 30bbee7

Browse files
authored
Merge pull request #208 from MorpheusAIs/fix/remove-cognito-auto-refresh
feat: remove PII from database, resolve email from Cognito live
2 parents 0a92974 + 52df766 commit 30bbee7

11 files changed

Lines changed: 134 additions & 438 deletions

File tree

.github/workflows/build.yml

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -356,37 +356,35 @@ jobs:
356356
run: |
357357
BUILDTAG=${{ needs.Generate-Tag.outputs.tag_name }}
358358
359-
# Determine environment and database
360-
# Note: Using Aurora Serverless v2 endpoints (cypher.*.mor.org)
361-
# Aurora has replaced the old RDS instances (db.*.mor.org)
359+
# Determine environment from branch
362360
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
363361
ENV="dev"
364-
DB_HOST="cypher.dev.mor.org"
365362
elif [ "${{ github.ref_name }}" == "stg" ]; then
366363
ENV="stg"
367-
DB_HOST="cypher.stg.mor.org"
368364
elif [ "${{ github.ref_name }}" == "main" ]; then
369365
ENV="prd"
370-
DB_HOST="cypher.mor.org"
371366
else
372367
echo "❌ Unsupported branch for deployment: ${{ github.ref_name }}"
373368
exit 1
374369
fi
375370
376-
echo "🗄️ Running database migrations for environment: $ENV"
377-
echo "📍 Database host: $DB_HOST"
378-
379-
# Get database credentials from AWS Secrets Manager
371+
# Get database connection details from the dedicated DB-creds secret
372+
# (contains only POSTGRES_USER/PASSWORD/DB/HOST/PORT — no app secrets)
380373
SECRET_VALUE=$(aws secretsmanager get-secret-value \
381-
--secret-id "${ENV}-morpheus-api" \
374+
--secret-id "${ENV}-morpheus-api-rds-proxy-credentials" \
382375
--query SecretString --output text)
383376
384377
DB_USER=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_USER')
385378
DB_PASSWORD=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PASSWORD')
386379
DB_NAME=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_DB')
380+
DB_HOST=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_HOST')
381+
DB_PORT=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PORT')
382+
383+
echo "🗄️ Running database migrations for environment: $ENV"
384+
echo "📍 Database host: $DB_HOST"
387385
388386
# Set database URL for migrations
389-
export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/${DB_NAME}"
387+
export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
390388
export ENVIRONMENT="$ENV"
391389
392390
# Create backup point (get current revision before migration)
@@ -793,36 +791,33 @@ jobs:
793791
exit 0
794792
fi
795793
796-
# Determine environment and database
797-
# Note: Using Aurora Serverless v2 endpoints (cypher.*.mor.org)
798-
# Aurora has replaced the old RDS instances (db.*.mor.org)
794+
# Determine environment from branch
799795
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
800796
ENV="dev"
801-
DB_HOST="cypher.dev.mor.org"
802797
elif [ "${{ github.ref_name }}" == "stg" ]; then
803798
ENV="stg"
804-
DB_HOST="cypher.stg.mor.org"
805799
elif [ "${{ github.ref_name }}" == "main" ]; then
806800
ENV="prd"
807-
DB_HOST="cypher.mor.org"
808801
else
809802
echo "❌ Unknown environment for rollback"
810803
exit 1
811804
fi
812805
813-
echo "📍 Rolling back database in environment: $ENV"
814-
echo "🔄 Target rollback revision: ${{ env.PRE_MIGRATION_REVISION }}"
815-
816-
# Get database credentials from AWS Secrets Manager
806+
# Get database connection details from the dedicated DB-creds secret
817807
SECRET_VALUE=$(aws secretsmanager get-secret-value \
818-
--secret-id "${ENV}-morpheus-api" \
808+
--secret-id "${ENV}-morpheus-api-rds-proxy-credentials" \
819809
--query SecretString --output text)
820810
821811
DB_USER=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_USER')
822812
DB_PASSWORD=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PASSWORD')
823813
DB_NAME=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_DB')
814+
DB_HOST=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_HOST')
815+
DB_PORT=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PORT')
816+
817+
echo "📍 Rolling back database in environment: $ENV"
818+
echo "🔄 Target rollback revision: ${{ env.PRE_MIGRATION_REVISION }}"
824819
825-
export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:5432/${DB_NAME}"
820+
export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
826821
827822
# Check current revision before rollback
828823
CURRENT_REV=$(poetry run alembic current --verbose 2>/dev/null | grep "Current revision" | awk '{print $NF}' || echo "none")
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Drop email and name columns from users table
2+
3+
PII (email, name) is managed exclusively in Cognito. The API resolves email
4+
on-demand via the user's access token when needed (e.g. GET /me). The database
5+
only stores cognito_user_id as the identity key.
6+
7+
Revision ID: drop_email_name_2026
8+
Revises: e1f2a3b4c5d6
9+
Create Date: 2026-03-05 00:01:00.000000
10+
"""
11+
from typing import Sequence, Union
12+
13+
from alembic import op
14+
import sqlalchemy as sa
15+
16+
17+
revision: str = 'drop_email_name_2026'
18+
down_revision: Union[str, None] = 'e1f2a3b4c5d6'
19+
branch_labels: Union[str, Sequence[str], None] = None
20+
depends_on: Union[str, Sequence[str], None] = None
21+
22+
23+
def upgrade() -> None:
24+
op.drop_index('ix_users_email_nonunique', table_name='users')
25+
op.drop_column('users', 'email')
26+
op.drop_column('users', 'name')
27+
28+
29+
def downgrade() -> None:
30+
op.add_column('users', sa.Column('name', sa.String(), nullable=True))
31+
op.add_column('users', sa.Column('email', sa.String(), nullable=True))
32+
op.create_index('ix_users_email_nonunique', 'users', ['email'], unique=False)

src/api/v1/auth/index.py

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import datetime, timezone
44

55
from fastapi import APIRouter, HTTPException, status, Depends, Body, Request, Response, Query
6+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
67
from sqlalchemy.ext.asyncio import AsyncSession
78

89
from ....crud import user as user_crud
@@ -20,43 +21,40 @@
2021

2122
router = APIRouter(tags=["Auth"])
2223

23-
# Note: Authentication is now handled by Cognito
24-
# Users authenticate via Cognito OAuth2 flow and receive JWT tokens
25-
# The frontend should redirect to Cognito for login/registration
26-
27-
# OAuth2 callback is handled by the /docs/oauth2-redirect endpoint
24+
_bearer = HTTPBearer(auto_error=False)
2825

2926
@router.get("/me", response_model=dict)
3027
async def get_current_user_info(
3128
current_user: User = Depends(get_current_user),
32-
db: AsyncSession = Depends(get_db_session)
29+
token: Optional[HTTPAuthorizationCredentials] = Depends(_bearer),
3330
):
3431
"""
3532
Get current user information.
36-
37-
Requires JWT Bearer authentication with Cognito token.
38-
User data is automatically kept up-to-date during authentication.
33+
34+
Email and name are fetched live from Cognito (not stored in DB).
35+
Uses the caller's own access token — same pattern as the frontend.
3936
"""
40-
user = current_user
41-
data_source = "database_auto_updated"
42-
43-
# Return user data focusing on email and cognito_id
44-
response_data = {
45-
"id": user.id,
46-
"cognito_user_id": user.cognito_user_id,
47-
"email": user.email,
48-
"name": user.name,
49-
"is_active": user.is_active,
50-
"age_verified": user.age_verified,
51-
"age_verified_at": user.age_verified_at,
52-
"created_at": user.created_at,
53-
"updated_at": user.updated_at,
54-
"data_source": data_source
37+
email = None
38+
name = None
39+
40+
if token:
41+
cognito_info = await cognito_service.get_user_by_token(token.credentials)
42+
if cognito_info:
43+
email = cognito_info.get('email')
44+
name = cognito_info.get('name')
45+
46+
return {
47+
"id": current_user.id,
48+
"cognito_user_id": current_user.cognito_user_id,
49+
"email": email,
50+
"name": name,
51+
"is_active": current_user.is_active,
52+
"age_verified": current_user.age_verified,
53+
"age_verified_at": current_user.age_verified_at,
54+
"created_at": current_user.created_at,
55+
"updated_at": current_user.updated_at,
56+
"data_source": "cognito_live",
5557
}
56-
57-
# Email should now be automatically updated during authentication
58-
59-
return response_data
6058

6159
@router.post("/verify-age", response_model=dict)
6260
async def verify_age(
@@ -383,7 +381,7 @@ async def get_default_api_key_decrypted(
383381
"name": api_key_obj.name,
384382
"created_at": api_key_obj.created_at
385383
},
386-
"suggestion": "Try refreshing your user data with GET /api/v1/auth/me?refresh_from_cognito=true, or create a new API key"
384+
"suggestion": "Try creating a new API key using POST /api/v1/auth/keys"
387385
}
388386

389387
# Success case

src/core/config.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,8 @@ def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str
164164
PROXY_ROUTER_CHAT_TIMEOUT: float = Field(default=float(os.getenv("PROXY_ROUTER_CHAT_TIMEOUT", "300.0")))
165165
PROXY_ROUTER_STREAM_TIMEOUT: float = Field(default=float(os.getenv("PROXY_ROUTER_STREAM_TIMEOUT", "300.0")))
166166

167-
# AWS settings
167+
# AWS settings (credentials come from ECS task role; no explicit keys needed)
168168
AWS_REGION: str = os.getenv("AWS_REGION", "us-east-2")
169-
AWS_ACCESS_KEY_ID: str | None = Field(default=os.getenv("AWS_ACCESS_KEY_ID"))
170-
AWS_SECRET_ACCESS_KEY: str | None = Field(default=os.getenv("AWS_SECRET_ACCESS_KEY"))
171-
AWS_SESSION_TOKEN: str | None = Field(default=os.getenv("AWS_SESSION_TOKEN"))
172169

173170
# AWS Cognito Settings
174171
COGNITO_USER_POOL_ID: str = Field(default=os.getenv("COGNITO_USER_POOL_ID", "us-east-2_tqCTHoSST"))

src/core/local_testing.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,9 @@ async def get_or_create_test_user(db: AsyncSession) -> User:
3333
test_user = await user_crud.get_user_by_cognito_id(db, "local-test-user")
3434

3535
if not test_user:
36-
# Create test user
37-
user_data = {
38-
'cognito_user_id': 'local-test-user',
39-
'email': 'test@local.dev',
40-
'name': 'Local Test User'
41-
}
42-
test_user = await user_crud.create_user_from_cognito(db, user_data)
36+
test_user = await user_crud.create_user_from_cognito(db, 'local-test-user')
4337
logger.info("Created test user for local development",
4438
test_user_id=test_user.id,
45-
test_email=user_data['email'],
4639
event_type="test_user_created")
4740

4841
return test_user
@@ -52,7 +45,7 @@ def log_local_testing_status():
5245
if is_local_testing_mode():
5346
logger.warning("LOCAL TESTING MODE ACTIVE",
5447
bypass_cognito=True,
55-
test_user_email="test@local.dev",
48+
test_cognito_id="local-test-user",
5649
production_safe=False,
5750
event_type="local_testing_active")
5851
logger.warning("Cognito authentication BYPASSED - NOT FOR PRODUCTION USE",

src/crud/api_key.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,6 @@ async def get_decrypted_api_key(db: AsyncSession, api_key_id: int, user_id: int)
357357
api_key_id=api_key_id,
358358
user_id=user_id,
359359
cognito_user_id=api_key.user.cognito_user_id[:8] + "...",
360-
user_email=api_key.user.email,
361360
encrypted_key_length=len(api_key.encrypted_key),
362361
event_type="api_key_found")
363362

0 commit comments

Comments
 (0)