Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
260d5c7
Draft one: modify model, added crud, route and fix deps, migration
avirajsingh7 Oct 3, 2025
fc36a28
pre commit
avirajsingh7 Oct 3, 2025
600d84a
Add permission management for API key routes and user context dependency
avirajsingh7 Oct 3, 2025
027565c
Refactor API key management: replace verify_api_key function with API…
avirajsingh7 Oct 6, 2025
b388cad
Enhance API key management: add support for old key format in APIKeyM…
avirajsingh7 Oct 6, 2025
87f9b70
Fix API key generation: ensure exact lengths for prefix and secret ke…
avirajsingh7 Oct 6, 2025
17063f1
Add migration script for API keys: convert encrypted keys to hashed f…
avirajsingh7 Oct 6, 2025
338be0e
precommit
avirajsingh7 Oct 6, 2025
1e90f01
Refactor API key handling in tests to use AuthContext
avirajsingh7 Oct 6, 2025
14eb31c
Fix API key route prefix and improve formatting in auth context funct…
avirajsingh7 Oct 6, 2025
e9376cd
move APIKeyManager to security module and clean up imports in CRUD op…
avirajsingh7 Oct 7, 2025
b3237c3
user context handling to use AuthContext, updating dependencies and v…
avirajsingh7 Oct 7, 2025
029c00e
rename AuthContext to TestAuthContext for consistency in test files
avirajsingh7 Oct 7, 2025
8ecf9ea
API key creation to include user and project ID parameters, enhancing…
avirajsingh7 Oct 7, 2025
60ba6f5
enhance test coverage for API key CRUD and Routes operations
avirajsingh7 Oct 7, 2025
1cb5091
Add tests for API key manager
avirajsingh7 Oct 7, 2025
520af6d
authentication context functions for consistency in test files
avirajsingh7 Oct 7, 2025
d94adec
replace APIKeyCrud with create_test_api_key for consistency in test s…
avirajsingh7 Oct 7, 2025
557c143
Add tests for get_user_context
avirajsingh7 Oct 7, 2025
b94c1d9
Add tests for permission checks and permission enum functionality
avirajsingh7 Oct 7, 2025
1c128e9
pre commt
avirajsingh7 Oct 7, 2025
fe288b3
Fix read_one method docstring and update logging for API key creation
avirajsingh7 Oct 8, 2025
76ac22e
Update API key deletion to set updated_at timestamp and modify user_i…
avirajsingh7 Oct 8, 2025
786992e
Fix import statement for get_project_by_id in APIKeyCrud
avirajsingh7 Oct 8, 2025
45df0c3
Merge remote-tracking branch 'origin/main' into refactor/authentication
avirajsingh7 Oct 10, 2025
543d07e
pre commit
avirajsingh7 Oct 10, 2025
32ffe9f
fix migration
avirajsingh7 Oct 10, 2025
1fa1c32
precommit
avirajsingh7 Oct 10, 2025
f51bbcd
Refactor API key handling and improve documentation
avirajsingh7 Oct 16, 2025
c19da66
Merge remote-tracking branch 'origin/main' into refactor/authentication
avirajsingh7 Oct 16, 2025
417c15c
Update APIKeyManager docstring and enhance seed data comment for clarity
avirajsingh7 Oct 16, 2025
8ce8a12
fix migration head
avirajsingh7 Oct 16, 2025
01f705a
precomit
avirajsingh7 Oct 16, 2025
04126d0
Refactor API key migration to use context manager for session handlin…
avirajsingh7 Oct 16, 2025
890bbd5
precommit
avirajsingh7 Oct 16, 2025
fb7fe60
remove id's from authcontext
avirajsingh7 Oct 16, 2025
645b510
Remove user_id from AuthContext instantiation in get_auth_context fun…
avirajsingh7 Oct 16, 2025
e91fd5c
Update user authentication to check for active status before returnin…
avirajsingh7 Oct 17, 2025
34632a1
precommit
avirajsingh7 Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions backend/app/alembic/migrate_api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"""
Migration script to convert encrypted API keys to hashed format.

This script:
1. Decrypts existing API keys from the old encrypted format
2. Extracts the prefix and secret from the decrypted keys
3. Hashes the secret using bcrypt
4. Generates UUID4 for the new primary key
5. Stores the prefix, hash, and UUID in the new format for backward compatibility

The format is: "ApiKey {12-char-prefix}{31-char-secret}" (total 43 chars)
"""

import logging
import uuid
from sqlalchemy.orm import Session
from sqlalchemy import text
from passlib.context import CryptContext

from app.core.security import decrypt_api_key

logger = logging.getLogger(__name__)

# Use the same hash algorithm as APIKeyManager
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# Old format constants
OLD_PREFIX_NAME = "ApiKey "
OLD_PREFIX_LENGTH = 12
OLD_SECRET_LENGTH = 31
OLD_KEY_LENGTH = 43 # Total: 12 + 31


def migrate_api_keys(session: Session, generate_uuid: bool = False) -> None:
"""
Migrate all existing API keys from encrypted format to hashed format.

This function:
1. Fetches all API keys with the old 'key' column
2. Decrypts each key
3. Extracts prefix and secret
4. Hashes the secret
5. Generates UUID4 for new_id column if generate_uuid is True
6. Updates key_prefix, key_hash, and optionally new_id columns

Args:
session: SQLAlchemy database session
generate_uuid: Whether to generate and set UUID for new_id column
"""
logger.info(
"[migrate_api_keys] Starting API key migration from encrypted to hashed format"
)

try:
# Fetch all API keys that have the old 'key' column
result = session.execute(
text("SELECT id, key FROM apikey WHERE key IS NOT NULL")
)
api_keys = result.fetchall()

if not api_keys:
logger.info("[migrate_api_keys] No API keys found to migrate")
return

logger.info(f"[migrate_api_keys] Found {len(api_keys)} API keys to migrate")

migrated_count = 0
failed_count = 0

for row in api_keys:
key_id = row[0]
encrypted_key = row[1]

try:
# Decrypt the API key
decrypted_key = decrypt_api_key(encrypted_key)

# Validate format
if not decrypted_key.startswith(OLD_PREFIX_NAME):
logger.error(
f"[migrate_api_keys] Invalid key format for ID {key_id}: "
f"does not start with '{OLD_PREFIX_NAME}'"
)
failed_count += 1
continue

# Extract the key part (after "ApiKey ")
key_part = decrypted_key[len(OLD_PREFIX_NAME) :]

if len(key_part) != OLD_KEY_LENGTH:
logger.error(
f"[migrate_api_keys] Invalid key length for ID {key_id}: "
f"expected {OLD_KEY_LENGTH}, got {len(key_part)}"
)
failed_count += 1
continue

# Extract prefix and secret
key_prefix = key_part[:OLD_PREFIX_LENGTH]
secret_key = key_part[OLD_PREFIX_LENGTH:]

# Hash the secret
key_hash = pwd_context.hash(secret_key)

# Generate UUID if requested
if generate_uuid:
new_uuid = uuid.uuid4()
# Update the record with prefix, hash, and UUID
session.execute(
text(
"UPDATE apikey SET key_prefix = :prefix, key_hash = :hash, new_id = :new_id "
"WHERE id = :id"
),
{
"prefix": key_prefix,
"hash": key_hash,
"new_id": new_uuid,
"id": key_id,
},
)
else:
# Update the record with prefix and hash only
session.execute(
text(
"UPDATE apikey SET key_prefix = :prefix, key_hash = :hash "
"WHERE id = :id"
),
{"prefix": key_prefix, "hash": key_hash, "id": key_id},
)

migrated_count += 1
logger.info(
f"[migrate_api_keys] Successfully migrated key ID {key_id} "
f"with prefix {key_prefix[:4]}..."
)

except Exception as e:
logger.error(
f"[migrate_api_keys] Failed to migrate key ID {key_id}: {str(e)}",
exc_info=True,
)
failed_count += 1
continue

logger.info(
f"[migrate_api_keys] Migration completed: "
f"{migrated_count} successful, {failed_count} failed"
)

except Exception as e:
logger.error(
f"[migrate_api_keys] Fatal error during migration: {str(e)}", exc_info=True
)
raise


def verify_migration(session: Session) -> bool:
"""
Verify that all API keys have been migrated successfully.

Args:
session: SQLAlchemy database session

Returns:
bool: True if all keys are migrated, False otherwise
"""
try:
# Check for any keys with NULL key_prefix or key_hash
result = session.execute(
text(
"SELECT COUNT(*) FROM apikey "
"WHERE key_prefix IS NULL OR key_hash IS NULL"
)
)
null_count = result.scalar()

if null_count > 0:
logger.warning(
f"[verify_migration] Found {null_count} API keys with NULL "
"key_prefix or key_hash"
)
return False

# Check total count
result = session.execute(text("SELECT COUNT(*) FROM apikey"))
total_count = result.scalar()

logger.info(
f"[verify_migration] All {total_count} API keys have been "
"successfully migrated"
)
return True

except Exception as e:
logger.error(
f"[verify_migration] Error verifying migration: {str(e)}", exc_info=True
)
return False
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Refactor API key table

Revision ID: e7c68e43ce6f
Revises: 27c271ab6dd0
Create Date: 2025-10-16 13:06:51.777671

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.orm import Session
from app.alembic.migrate_api_key import migrate_api_keys, verify_migration


# revision identifiers, used by Alembic.
revision = "e7c68e43ce6f"
down_revision = "27c271ab6dd0"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
# Step 1: Add new columns as nullable to allow migration
op.add_column(
"apikey",
sa.Column("key_prefix", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
)
op.add_column(
"apikey",
sa.Column("key_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
)

# Step 2: Add UUID column before migration
op.add_column("apikey", sa.Column("new_id", sa.Uuid(), nullable=True))

# Step 3: Migrate existing encrypted keys to the new hashed format and generate UUIDs
bind = op.get_bind()
with Session(bind=bind) as session:
migrate_api_keys(session, generate_uuid=True)

# Step 4: Verify migration was successful
if not verify_migration(session):
raise Exception(
"API key migration verification failed. Please check the logs."
)

session.flush()

# Step 5: Make the columns non-nullable after migration
op.alter_column("apikey", "key_prefix", nullable=False)
op.alter_column("apikey", "key_hash", nullable=False)

# Step 6: Replace old PK with UUID-based PK
op.drop_constraint("apikey_pkey", "apikey", type_="primary")
op.drop_column("apikey", "id")
op.alter_column("apikey", "new_id", new_column_name="id", nullable=False)
op.create_primary_key("apikey_pkey", "apikey", ["id"])

# Step 7: Update indexes and drop old key column
op.drop_index("ix_apikey_key", table_name="apikey")
op.create_index(op.f("ix_apikey_key_prefix"), "apikey", ["key_prefix"], unique=True)
op.drop_column("apikey", "key")
# ### end Alembic commands ###


def downgrade():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be something over here which does the reverse of migrate_api_keys?

what would be the repurcussions if we had to rollback and did nothing on downgrade?

Copy link
Collaborator Author

@avirajsingh7 avirajsingh7 Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of migration we will use db snapshot for downgrade.
Downgrade through script not possible due to hashing.
Added a comment.

# instead of downgrade, will take a db snapshot and restore from that if needed
pass
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the downgrade here empty

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refer here

Loading