diff --git a/deployment/terraform/terraform.tfstate b/deployment/terraform/terraform.tfstate index a797076..0eda6e6 100644 --- a/deployment/terraform/terraform.tfstate +++ b/deployment/terraform/terraform.tfstate @@ -1,7 +1,7 @@ { "version": 4, "terraform_version": "1.10.1", - "serial": 393, + "serial": 399, "lineage": "a183cd95-f987-8698-c6dd-84e933c394a5", "outputs": { "cloud_function_name": { @@ -824,7 +824,7 @@ }, { "type": "get_attr", - "value": "disk_encryption_key_raw" + "value": "disk_encryption_key_rsa" } ], [ @@ -841,7 +841,7 @@ }, { "type": "get_attr", - "value": "disk_encryption_key_rsa" + "value": "disk_encryption_key_raw" } ] ], @@ -870,8 +870,8 @@ "database_edition": "STANDARD", "delete_protection_state": "DELETE_PROTECTION_DISABLED", "deletion_policy": "ABANDON", - "earliest_version_time": "2025-05-24T21:09:24.677010Z", - "etag": "IPScrICPvY0DMKrW4vCEvY0D", + "earliest_version_time": "2025-05-24T21:16:19.051906Z", + "etag": "INn0mIORvY0DMKrW4vCEvY0D", "id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb", "key_prefix": "", "location_id": "us-central1", @@ -1505,7 +1505,7 @@ "md5hash": "NNgXJau9T0I95x7NQhXRFg==", "md5hexhash": "34d81725abbd4f423de71ecd4215d116", "media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-34d81725abbd4f423de71ecd4215d116.zip?generation=1748124439573408\u0026alt=media", - "metadata": null, + "metadata": {}, "name": "function-source-34d81725abbd4f423de71ecd4215d116.zip", "output_name": "function-source-34d81725abbd4f423de71ecd4215d116.zip", "retention": [], diff --git a/deployment/terraform/terraform.tfstate.backup b/deployment/terraform/terraform.tfstate.backup index efd4d4b..6e488a6 100644 --- a/deployment/terraform/terraform.tfstate.backup +++ b/deployment/terraform/terraform.tfstate.backup @@ -1,7 +1,7 @@ { "version": 4, "terraform_version": "1.10.1", - "serial": 388, + "serial": 397, "lineage": "a183cd95-f987-8698-c6dd-84e933c394a5", "outputs": { "cloud_function_name": { @@ -98,16 +98,16 @@ "attributes": { "exclude_symlink_directories": null, "excludes": null, - "id": "ebb70c54eaebd24049805bcc1425349f70bc582d", - "output_base64sha256": "+Q18L9q1o61gbnGJlSvTmwG9cRv1Qwzf8GI95No2Rb4=", - "output_base64sha512": "tK0wkH07eL77+ytrOI8lcHATsN/nP0f/CYq0uzrrlhaRbJ+wsO1/6y0tmeX1hF6xqxW5ZDYTrhrSnayA+2afwQ==", + "id": "045029ac803155784c12f8d587fee56b85b1fbe9", + "output_base64sha256": "b/FgNMMT30JSXfrLRXNkWeNc6i22YAmT3YwQRTw1+A4=", + "output_base64sha512": "7GDDTkHwwQVAlwSxe7yzgtGccMNIRCQ7t72ZRk7bcfDI1tzpruhJ5G/0AbrUMXWQO6LffnWtwumQ7XdFHAIzBA==", "output_file_mode": null, - "output_md5": "95eb8ea5146b66f5b26bb830e3f0eab6", + "output_md5": "34d81725abbd4f423de71ecd4215d116", "output_path": "./function-source.zip", - "output_sha": "ebb70c54eaebd24049805bcc1425349f70bc582d", - "output_sha256": "f90d7c2fdab5a3ad606e7189952bd39b01bd711bf5430cdff0623de4da3645be", - "output_sha512": "b4ad30907d3b78befbfb2b6b388f25707013b0dfe73f47ff098ab4bb3aeb9616916c9fb0b0ed7feb2d2d99e5f5845eb1ab15b9643613ae1ad29dac80fb669fc1", - "output_size": 4781, + "output_sha": "045029ac803155784c12f8d587fee56b85b1fbe9", + "output_sha256": "6ff16034c313df42525dfacb45736459e35cea2db6600993dd8c10453c35f80e", + "output_sha512": "ec60c34e41f0c105409704b17bbcb382d19c70c34844243bb7bd99464edb71f0c8d6dce9aee849e46ff401bad43175903ba2df7e75adc2e990ed77451c023304", + "output_size": 5014, "source": [], "source_content": null, "source_content_filename": null, @@ -471,7 +471,7 @@ "automatic_update_policy": [ {} ], - "build": "projects/761163285547/locations/us-central1/builds/d3341e98-07e4-49de-8dc7-3d53e4d2570a", + "build": "projects/761163285547/locations/us-central1/builds/1b8e28d1-ee4d-4d2f-acf2-47e2b03aa421", "docker_repository": "projects/gen-lang-client-0424120530/locations/us-central1/repositories/gcf-artifacts", "entry_point": "process_image_embedding", "environment_variables": {}, @@ -485,7 +485,7 @@ { "bucket": "gen-lang-client-0424120530-cloud-function-source", "generation": 1748123369545880, - "object": "function-source-95eb8ea5146b66f5b26bb830e3f0eab6.zip" + "object": "function-source-34d81725abbd4f423de71ecd4215d116.zip" } ] } @@ -511,7 +511,7 @@ ], "id": "projects/gen-lang-client-0424120530/locations/us-central1/functions/process-image-embedding", "kms_key_name": "", - "labels": null, + "labels": {}, "location": "us-central1", "name": "process-image-embedding", "project": "gen-lang-client-0424120530", @@ -522,11 +522,17 @@ "available_memory": "512M", "binary_authorization_policy": "", "environment_variables": { + "FIRESTORE_DATABASE_NAME": "sereact-imagedb", + "FIRESTORE_PROJECT_ID": "gen-lang-client-0424120530", + "GCS_BUCKET_NAME": "sereact-images", "LOG_EXECUTION_ID": "true", + "LOG_LEVEL": "INFO", "QDRANT_API_KEY": "", "QDRANT_COLLECTION": "image_vectors", "QDRANT_HOST": "34.71.6.1", - "QDRANT_PORT": "6333" + "QDRANT_HTTPS": "false", + "QDRANT_PORT": "6333", + "VISION_API_ENABLED": "true" }, "gcf_uri": "", "ingress_settings": "ALLOW_ALL", @@ -548,7 +554,7 @@ "goog-terraform-provisioned": "true" }, "timeouts": null, - "update_time": "2025-05-24T21:52:26.933576416Z", + "update_time": "2025-05-24T22:08:16.899711009Z", "url": "https://us-central1-gen-lang-client-0424120530.cloudfunctions.net/process-image-embedding" }, "sensitive_attributes": [ @@ -801,7 +807,18 @@ [ { "type": "get_attr", - "value": "metadata_startup_script" + "value": "boot_disk" + }, + { + "type": "index", + "value": { + "value": 0, + "type": "number" + } + }, + { + "type": "get_attr", + "value": "disk_encryption_key_rsa" } ], [ @@ -824,18 +841,7 @@ [ { "type": "get_attr", - "value": "boot_disk" - }, - { - "type": "index", - "value": { - "value": 0, - "type": "number" - } - }, - { - "type": "get_attr", - "value": "disk_encryption_key_rsa" + "value": "metadata_startup_script" } ] ], @@ -864,8 +870,8 @@ "database_edition": "STANDARD", "delete_protection_state": "DELETE_PROTECTION_DISABLED", "deletion_policy": "ABANDON", - "earliest_version_time": "2025-05-24T21:09:24.677010Z", - "etag": "IOXfuveKvY0DMKrW4vCEvY0D", + "earliest_version_time": "2025-05-24T21:15:22.382696Z", + "etag": "IPeLluiQvY0DMKrW4vCEvY0D", "id": "projects/gen-lang-client-0424120530/databases/sereact-imagedb", "key_prefix": "", "location_id": "us-central1", @@ -896,7 +902,7 @@ "schema_version": 0, "attributes": { "condition": [], - "etag": "BwY16K9kGDo=", + "etag": "BwY16LCINIE=", "id": "gen-lang-client-0424120530/roles/eventarc.eventReceiver/serviceAccount:761163285547-compute@developer.gserviceaccount.com", "member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com", "project": "gen-lang-client-0424120530", @@ -920,7 +926,7 @@ "schema_version": 0, "attributes": { "condition": [], - "etag": "BwY16Kj5NHI=", + "etag": "BwY16LCINIE=", "id": "gen-lang-client-0424120530/roles/datastore.user/serviceAccount:761163285547-compute@developer.gserviceaccount.com", "member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com", "project": "gen-lang-client-0424120530", @@ -944,7 +950,7 @@ "schema_version": 0, "attributes": { "condition": [], - "etag": "BwY16K9kGDo=", + "etag": "BwY16LCINIE=", "id": "gen-lang-client-0424120530/roles/pubsub.subscriber/serviceAccount:761163285547-compute@developer.gserviceaccount.com", "member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com", "project": "gen-lang-client-0424120530", @@ -968,7 +974,7 @@ "schema_version": 0, "attributes": { "condition": [], - "etag": "BwY16Kj5NHI=", + "etag": "BwY16LCINIE=", "id": "gen-lang-client-0424120530/roles/storage.objectViewer/serviceAccount:761163285547-compute@developer.gserviceaccount.com", "member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com", "project": "gen-lang-client-0424120530", @@ -992,7 +998,7 @@ "schema_version": 0, "attributes": { "condition": [], - "etag": "BwY16Kj5NHI=", + "etag": "BwY16LCINIE=", "id": "gen-lang-client-0424120530/roles/ml.developer/serviceAccount:761163285547-compute@developer.gserviceaccount.com", "member": "serviceAccount:761163285547-compute@developer.gserviceaccount.com", "project": "gen-lang-client-0424120530", @@ -1489,21 +1495,21 @@ "content_encoding": "", "content_language": "", "content_type": "application/zip", - "crc32c": "tbmr3A==", + "crc32c": "YXAlNA==", "customer_encryption": [], - "detect_md5hash": "leuOpRRrZvWya7gw4/Dqtg==", + "detect_md5hash": "NNgXJau9T0I95x7NQhXRFg==", "event_based_hold": false, - "generation": 1748123256911890, - "id": "gen-lang-client-0424120530-cloud-function-source-function-source-95eb8ea5146b66f5b26bb830e3f0eab6.zip", + "generation": 1748124439573408, + "id": "gen-lang-client-0424120530-cloud-function-source-function-source-34d81725abbd4f423de71ecd4215d116.zip", "kms_key_name": "", - "md5hash": "leuOpRRrZvWya7gw4/Dqtg==", - "md5hexhash": "95eb8ea5146b66f5b26bb830e3f0eab6", - "media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-95eb8ea5146b66f5b26bb830e3f0eab6.zip?generation=1748123256911890\u0026alt=media", + "md5hash": "NNgXJau9T0I95x7NQhXRFg==", + "md5hexhash": "34d81725abbd4f423de71ecd4215d116", + "media_link": "https://storage.googleapis.com/download/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-34d81725abbd4f423de71ecd4215d116.zip?generation=1748124439573408\u0026alt=media", "metadata": {}, - "name": "function-source-95eb8ea5146b66f5b26bb830e3f0eab6.zip", - "output_name": "function-source-95eb8ea5146b66f5b26bb830e3f0eab6.zip", + "name": "function-source-34d81725abbd4f423de71ecd4215d116.zip", + "output_name": "function-source-34d81725abbd4f423de71ecd4215d116.zip", "retention": [], - "self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-95eb8ea5146b66f5b26bb830e3f0eab6.zip", + "self_link": "https://www.googleapis.com/storage/v1/b/gen-lang-client-0424120530-cloud-function-source/o/function-source-34d81725abbd4f423de71ecd4215d116.zip", "source": "./function-source.zip", "storage_class": "STANDARD", "temporary_hold": false, diff --git a/main.py b/main.py index 9982d28..96b2b64 100644 --- a/main.py +++ b/main.py @@ -140,16 +140,9 @@ def custom_openapi(): if "schemas" not in openapi_schema["components"]: openapi_schema["components"]["schemas"] = {} - # Apply security to endpoints except auth, users, teams, and API key creation endpoints - for path in openapi_schema["paths"]: - # Exclude auth endpoints, users endpoints, teams endpoints, and API key creation - if not (path.startswith("/api/v1/auth") or - path.startswith("/api/v1/users") or - path.startswith("/api/v1/teams")): - for method in openapi_schema["paths"][path]: - if method.lower() in ["get", "post", "put", "delete", "patch"]: - if "security" not in openapi_schema["paths"][path][method]: - openapi_schema["paths"][path][method]["security"] = [{"ApiKeyAuth": []}] + # Note: Authentication is now handled properly in individual route modules + # Public endpoints (auth, users, teams) don't require authentication + # Protected endpoints (images, search) require API key authentication app.openapi_schema = openapi_schema return app.openapi_schema diff --git a/src/api/v1/auth.py b/src/api/v1/auth.py index 9281b07..f17c7f4 100644 --- a/src/api/v1/auth.py +++ b/src/api/v1/auth.py @@ -10,7 +10,7 @@ from src.db.repositories.team_repository import team_repository from src.schemas.api_key import ApiKeyCreate, ApiKeyResponse, ApiKeyWithValueResponse, ApiKeyListResponse from src.schemas.team import TeamCreate from src.schemas.user import UserCreate -from src.auth.security import generate_api_key, verify_api_key, calculate_expiry_date, is_expired, hash_api_key +from src.auth.security import generate_api_key, verify_api_key, calculate_expiry_date, is_expired, hash_api_key, get_current_user from src.models.api_key import ApiKeyModel from src.models.team import TeamModel from src.models.user import UserModel @@ -20,43 +20,6 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=["Authentication"], prefix="/auth") -async def get_current_user(x_api_key: Optional[str] = Header(None)): - """ - Get the current user from API key - """ - if not x_api_key: - raise HTTPException(status_code=401, detail="API key is required") - - # Hash the API key - hashed_key = hash_api_key(x_api_key) - - # Get the key from the database - api_key = await api_key_repository.get_by_key_hash(hashed_key) - if not api_key: - raise HTTPException(status_code=401, detail="Invalid API key") - - # Check if the key is active - if not api_key.is_active: - raise HTTPException(status_code=401, detail="API key is inactive") - - # Check if the key has expired - if api_key.expiry_date and is_expired(api_key.expiry_date): - raise HTTPException(status_code=401, detail="API key has expired") - - # Get the user - user = await user_repository.get_by_id(api_key.user_id) - if not user: - raise HTTPException(status_code=401, detail="User not found") - - # Check if the user is active - if not user.is_active: - raise HTTPException(status_code=401, detail="User is inactive") - - # Update last used timestamp - await api_key_repository.update_last_used(api_key.id) - - return user - @router.post("/bootstrap", response_model=ApiKeyWithValueResponse, status_code=201) async def bootstrap_initial_setup( team_name: str, diff --git a/src/api/v1/images.py b/src/api/v1/images.py index be35d61..5cfa5eb 100644 --- a/src/api/v1/images.py +++ b/src/api/v1/images.py @@ -5,7 +5,7 @@ from fastapi.responses import StreamingResponse from bson import ObjectId import io -from src.api.v1.auth import get_current_user +from src.auth.security import get_current_user from src.db.repositories.image_repository import image_repository from src.services.storage import StorageService from src.services.image_processor import ImageProcessor diff --git a/src/api/v1/search.py b/src/api/v1/search.py index 0919888..ee25d83 100644 --- a/src/api/v1/search.py +++ b/src/api/v1/search.py @@ -2,7 +2,7 @@ import logging from typing import Optional, List, Dict, Any from fastapi import APIRouter, Depends, Query, Request, HTTPException -from src.api.v1.auth import get_current_user +from src.auth.security import get_current_user from src.services.vector_db import VectorDatabaseService from src.services.embedding_service import EmbeddingService from src.db.repositories.image_repository import image_repository diff --git a/src/auth/security.py b/src/auth/security.py index e42d0a2..98b509a 100644 --- a/src/auth/security.py +++ b/src/auth/security.py @@ -6,8 +6,15 @@ import hashlib from datetime import datetime, timedelta from typing import Optional, Tuple +from fastapi import HTTPException, Security, Depends +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from fastapi.security.api_key import APIKeyHeader + from src.config.config import settings +# API Key authentication scheme +api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) + def generate_api_key(team_id: str, user_id: str) -> Tuple[str, str]: """ Generate a secure API key and its hashed value @@ -88,4 +95,81 @@ def is_expired(expiry_date: datetime) -> bool: Returns: True if expired """ - return datetime.utcnow() > expiry_date \ No newline at end of file + return datetime.utcnow() > expiry_date + +async def get_api_key(api_key: Optional[str] = Security(api_key_header)) -> str: + """ + Dependency to extract and validate API key from request headers + + Args: + api_key: API key from X-API-Key header + + Returns: + Valid API key + + Raises: + HTTPException: If API key is missing or invalid + """ + if not api_key: + raise HTTPException( + status_code=401, + detail="API key is required" + ) + + return api_key + +async def get_current_user(api_key: str = Depends(get_api_key)): + """ + Dependency to get the current authenticated user from API key + + Args: + api_key: Valid API key + + Returns: + User model + + Raises: + HTTPException: If API key is invalid or user not found + """ + from src.db.repositories.api_key_repository import api_key_repository + from src.db.repositories.user_repository import user_repository + + # Find API key in database + api_key_obj = await api_key_repository.get_by_key_hash(hash_api_key(api_key)) + + if not api_key_obj: + raise HTTPException( + status_code=401, + detail="Invalid API key" + ) + + # Check if API key is expired + if is_expired(api_key_obj.expiry_date): + raise HTTPException( + status_code=401, + detail="API key has expired" + ) + + # Check if API key is active + if not api_key_obj.is_active: + raise HTTPException( + status_code=401, + detail="API key is inactive" + ) + + # Get user + user = await user_repository.get_by_id(api_key_obj.user_id) + if not user: + raise HTTPException( + status_code=401, + detail="User not found" + ) + + # Check if user is active + if not user.is_active: + raise HTTPException( + status_code=401, + detail="User account is inactive" + ) + + return user \ No newline at end of file