Engineer Secure Vector Search APIs in Python: Biology's Data Fortress

Engineer Secure Vector Search APIs in Python: Biology's Data Fortress

We stand at the frontier of biological discovery, where insights from genomics, proteomics, and drug design transform rapidly into actionable knowledge. Central to this revolution are vector search and similarity systems, decoding vast biological datasets to unveil hidden patterns and accelerate innovation. Yet, with this power comes profound responsibility: securing the integrity and confidentiality of sensitive biological information. Unprotected vector search APIs, the gateways to these invaluable data pipelines, represent critical vulnerabilities, inviting data breaches, intellectual property theft, and research compromise.


This resource mandates a proactive stance. We equip you to engineer secure vector search APIs with authentication in Python, transforming potential weaknesses into unyielding data fortresses. This guide delves into the strategic implementation of authentication, access control, and robust security practices tailored for the unique demands of biological and bio-engineering pipelines. We dissect core concepts, unveil actionable code examples, and share battle-tested strategies to safeguard your invaluable data. We navigate the intricate landscape of secure API development, ensuring that your biological breakthroughs remain protected. This systematic approach complements efforts to implement large-scale vector search for molecular and protein embeddings, fortifying the entire data lifecycle. Activate this knowledge to protect your innovation and command the security narrative of your bioinformatics infrastructure.

Activate Foundational Security: Blueprinting Biological Vector Search APIs

Activate Foundational Security: Blueprinting Biological Vector Search APIs

We initiate the construction of a secure vector search API by laying down a robust foundation. For biological and bio-engineering pipelines, data security transcends mere compliance; it becomes a strategic imperative. Compromised molecular structures, gene sequences, or drug compound embeddings can derail years of research, jeopardize intellectual property, and erode competitive advantage. Therefore, we forge our API with security as an intrinsic design principle, not an afterthought. Python, with frameworks like FastAPI, offers an agile and powerful environment for this endeavor, enabling us to engineer high-performance, asynchronous APIs with integrated security features.


Our primary objective in this phase is to establish the core API structure and implement a basic, yet effective, authentication mechanism. We leverage FastAPI's dependency injection system, a powerful paradigm that simplifies the integration of security components. This system allows us to define authentication logic once and apply it across multiple endpoints, ensuring consistency and reducing the surface area for errors. We commence with API key authentication, a common and straightforward method suitable for many internal and partner-facing applications. This involves validating an API key presented in the request header against a list of authorized keys. We activate a proactive posture by immediately rejecting unauthorized requests, preventing any access to sensitive vector search functionalities.


The provided code blueprint illustrates this initial setup. It defines a FastAPI application, configures API key handling, and creates protected endpoints for searching molecular and protein embeddings. Notice the `get_api_key` dependency; this function intercepts incoming requests, extracts the API key, and validates it. If validation fails, it immediately raises an HTTP 401 Unauthorized error, decisively blocking access. This fundamental layer forms the bedrock upon which we will build more sophisticated security controls. We prioritize clarity and modularity, ensuring that each security component is identifiable and manageable, setting the stage for future enhancements in access control and threat mitigation.

import uvicorn
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import APIKeyHeader
from typing import Annotated

# --- Configuration and Environment Variables ---
# In a real-world scenario, retrieve API keys from environment variables
# or a secure configuration management system (e.g., AWS Secrets Manager, HashiCorp Vault).
# For demonstration, we use a simple hardcoded list.

# Define valid API keys (production: use strong, unique keys)
API_KEYS = [
    "sec_api_key_for_research_team",
    "sec_api_key_for_drug_discovery"
]

# Define API Key header name
API_KEY_NAME = "X-API-Key"

# Initialize APIKeyHeader for FastAPI dependency injection
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True)

# --- Dependency for API Key Authentication ---
# This function validates the incoming API key from the request header.
async def get_api_key(api_key: Annotated[str, Depends(api_key_header)]) -> str:
    if api_key not in API_KEYS:
        # Raise an HTTPException if the API key is invalid or missing
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid API Key",
        )
    return api_key

# --- FastAPI Application Initialization ---
app = FastAPI(title="Secure Bio Vector Search API")

# --- API Endpoints ---
# A protected endpoint for searching molecular embeddings.
@app.get("/search/molecular", summary="Search for molecular embeddings with authentication")
async def search_molecular_embeddings(query_vector: str, top_k: int = 5, api_key: Annotated[str, Depends(get_api_key)]):
    """
    Performs a vector similarity search across a molecular embedding database.
    Requires a valid API key in the 'X-API-Key' header.
    
    Args:
        query_vector (str): A string representation of the query vector (e.g., JSON, comma-separated).
        top_k (int): The number of top similar results to retrieve.
        
    Returns:
        dict: A dictionary containing search results (placeholder for actual vector search logic).
    """
    # In a real application, integrate with your vector database (e.g., Pinecone, Milvus, Qdrant).
    # This is a placeholder for the actual vector search logic.
    print(f"Authenticated request for molecular search with API key: {api_key}")
    print(f"Query: {query_vector}, Top-K: {top_k}")
    return {"status": "success", "message": "Molecular embeddings search executed securely.", "results": []}

# A protected endpoint for searching protein embeddings.
@app.get("/search/protein", summary="Search for protein embeddings with authentication")
async def search_protein_embeddings(query_vector: str, top_k: int = 5, api_key: Annotated[str, Depends(get_api_key)]):
    """
    Performs a vector similarity search across a protein embedding database.
    Requires a valid API key in the 'X-API-Key' header.
    """
    print(f"Authenticated request for protein search with API key: {api_key}")
    print(f"Query: {query_vector}, Top-K: {top_k}")
    return {"status": "success", "message": "Protein embeddings search executed securely.", "results": []}

# A public, unprotected health check endpoint (optional but good practice).
@app.get("/health", summary="API Health Check (unprotected)")
async def health_check():
    return {"status": "healthy", "version": "1.0.0"}

# --- Run the FastAPI application ---
# To run this application:
# 1. Save the code as 'main.py'.
# 2. Install FastAPI and Uvicorn: pip install "fastapi[all]"
# 3. Run from your terminal: uvicorn main:app --reload
#    Then access your API at http://127.0.0.1:8000/docs
Implement Robust Authentication: Fortifying Access to Biological Insights

Implement Robust Authentication: Fortifying Access to Biological Insights

We escalate our security posture by implementing robust authentication mechanisms, vital for safeguarding sensitive biological data pipelines. While API keys offer a practical first line of defense, advanced scenarios demand more sophisticated strategies. Here, we delve into two powerful methods: API Key enhancements and JSON Web Tokens (JWT). Each method empowers specific use cases, and understanding their nuances allows us to deploy the optimal defense for our bioinformatics infrastructure.


For API keys, we evolve beyond simple validation. We infuse them with contextual metadata, such as assigned scopes or team affiliations, transforming them into intelligent keys that convey not just identity but also potential permissions. This preliminary step towards authorization strengthens the API key's utility. However, for client applications, single-page applications, or scenarios demanding identity delegation and expiry, JWTs emerge as a superior choice. JWTs encapsulate user identity and claims (like roles or permissions) within a cryptographically signed token. This token, once issued, can be validated by the API without constant database lookups, optimizing performance while maintaining integrity. We issue these tokens via a dedicated login endpoint, requiring user credentials (e.g., username/password) to generate a time-limited access token.


The provided code refines our API Key dependency to retrieve associated metadata and introduces the entire JWT flow. This includes functions for creating and decoding tokens, along with a /token endpoint where users can exchange credentials for a JWT. Our protected API endpoints now leverage either API Key information or a decoded JWT payload to authenticate requests. This dual-pronged approach allows flexibility: certain pipelines might operate effectively with API keys for machine-to-machine communication, while interactive bioinformatics applications benefit immensely from the session-management and delegated authority capabilities of JWTs. We ensure that each authentication method decisively verifies the requesting entity, blocking unauthorized access to the invaluable biological vector search capabilities.

import uvicorn
from fastapi import FastAPI, Depends, HTTPException, status, Security
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer, OAuth2PasswordRequestForm
from typing import Annotated
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt

# --- Configuration for API Keys (from previous part) ---
API_KEYS = {
    "research_api_key": {"scope": "molecular_read", "team": "research"},
    "drug_discovery_api_key": {"scope": "protein_read", "team": "drug_discovery"}
}
API_KEY_NAME = "X-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) # Changed to auto_error=False for custom response

# --- Configuration for JWT ---
SECRET_KEY = "super-secret-key-replace-with-env-variable"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# OAuth2PasswordBearer for getting the token from the Authorization header
# 'tokenUrl' points to the endpoint where clients can get a token
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# --- Helper Functions for JWT ---
def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise JWTError("Invalid token: missing subject")
        return payload
    except JWTError as e:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Could not validate credentials: {e}")

# --- FastAPI Application Initialization ---
app = FastAPI(title="Secure Bio Vector Search API")

# --- Authentication Dependencies ---
# Dependency for API Key Authentication (now with scope awareness)
async def get_api_key(api_key: Annotated[str, Security(api_key_header)]) -> dict:
    if api_key not in API_KEYS:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid API Key or no API Key provided. Please provide a valid 'X-API-Key'.",
            headers={"WWW-Authenticate": "Bearer"}
        )
    return API_KEYS[api_key]

# Dependency for JWT Token Authentication
async def get_current_user_payload(token: Annotated[str, Depends(oauth2_scheme)]) -> dict:
    return decode_access_token(token)

# --- Token Endpoint for JWT ---
@app.post("/token", summary="Generate an access token for user authentication")
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> dict:
    # In a real app, validate username/password against a secure user database
    # For this example, we'll use dummy credentials
    if form_data.username == "bio_user" and form_data.password == "strong_password_123":
        access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
        access_token = create_access_token(
            data={"sub": form_data.username, "role": "molecular_analyst"},
            expires_delta=access_token_expires
        )
        return {"access_token": access_token, "token_type": "bearer"}
    raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Incorrect username or password",
        headers={"WWW-Authenticate": "Bearer"},
    )

# --- API Endpoints with both API Key and JWT protection ---
# Protected endpoint for molecular search, requiring either a specific API key or a JWT.
@app.get("/search/molecular", summary="Search molecular embeddings (API Key or JWT)")
async def search_molecular_embeddings(
    query_vector: str, top_k: int = 5,
    api_key_info: Annotated[dict, Depends(get_api_key)],
    current_user_payload: Annotated[dict, Depends(get_current_user_payload)]
):
    # This example demonstrates how to *potentially* allow both.
    # In a real scenario, you'd typically choose one or define a clear priority/fallback.
    # For simplicity, let's say if a valid API key is present, it takes precedence,
    # or if the user has a valid JWT, that's sufficient.

    # For robust security, we enforce: if a user is authenticated via JWT OR API Key, proceed.
    # We could also add role-based authorization here.
    auth_method = "API Key" if api_key_info else "JWT Token"
    auth_identity = api_key_info.get("team") if api_key_info else current_user_payload.get("sub")

    # Here, we'd add specific authorization checks based on roles/scopes from api_key_info or current_user_payload
    if not auth_identity: # Should not happen if dependencies passed, but good for type safety
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No valid authentication context provided.")

    print(f"Authenticated request via {auth_method} by {auth_identity} for molecular search.")
    return {"status": "success", "message": "Molecular embeddings search executed securely.", "results": []}

# Protected endpoint for protein search, exclusively requiring JWT for this example.
@app.get("/search/protein", summary="Search protein embeddings (JWT only)")
async def search_protein_embeddings(
    query_vector: str, top_k: int = 5,
    current_user_payload: Annotated[dict, Depends(get_current_user_payload)]
):
    user_role = current_user_payload.get("role")
    if user_role not in ["molecular_analyst", "admin"]:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions for protein search.")

    print(f"Authenticated request via JWT by {current_user_payload.get('sub')} (Role: {user_role}) for protein search.")
    return {"status": "success", "message": "Protein embeddings search executed securely.", "results": []}

# --- Run the FastAPI application ---
# To run this application:
# 1. Save the code as 'main.py'.
# 2. Install FastAPI, Uvicorn, and python-jose: pip install "fastapi[all]" python-jose[cryptography]
# 3. Run from your terminal: uvicorn main:app --reload
#    Then access your API at http://127.0.0.1:8000/docs

Forge Fine-Grained Access Control: Orchestrating Authorization for Biological Data

Authentication establishes who is requesting access; authorization determines what that authenticated entity is permitted to do. For biological vector search pipelines, this distinction is critical. We must ensure that a researcher working on molecular interactions cannot inadvertently (or maliciously) access proprietary drug compound embeddings, nor can a contractor accidentally modify critical genomic indices. We engineer fine-grained access control to precisely orchestrate permissions, safeguarding specific datasets and operations within our bio-engineering infrastructure.


We deploy Role-Based Access Control (RBAC) as our primary authorization strategy. RBAC simplifies permission management by assigning roles (e.g., 'researcher', 'drug_chemist', 'admin') to users or API keys, and then associating these roles with specific API endpoint access. This abstracts away individual permissions, making the system scalable and manageable. For instance, an 'admin' role might possess full read/write access across all biological embedding types, while a 'researcher' role is confined to reading molecular embeddings only. We embed these roles directly within our JWTs upon issuance and associate them with API keys in our configuration. This ensures that the authorization decision can be made efficiently at the API gateway, based on trusted claims.


The provided code introduces custom FastAPI dependencies, RoleChecker and APIKeyRoleChecker, which dynamically evaluate the roles extracted from either a JWT payload or API key data. These dependencies act as gatekeepers: if an authenticated entity lacks the necessary role for a specific endpoint, an HTTP 403 Forbidden error is immediately raised, decisively preventing unauthorized actions. We demonstrate this by segmenting access to molecular, protein, and genomic embedding operations based on defined roles. This layered security approach, combining authentication with granular authorization, transforms our API into a highly resilient fortress, ensuring that only authorized scientific exploration unfolds within defined boundaries. We validate permissions at every critical juncture, preventing unintended data exposure or manipulation.

import uvicorn
from fastapi import FastAPI, Depends, HTTPException, status, Security
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer, OAuth2PasswordRequestForm
from typing import Annotated
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt

# --- Configuration for API Keys ---
API_KEYS = {
    "research_api_key": {"scope": "molecular_read", "team": "research", "roles": ["researcher"]},
    "drug_discovery_api_key": {"scope": "protein_read", "team": "drug_discovery", "roles": ["drug_chemist"]},
    "admin_api_key": {"scope": "full_access", "team": "admin", "roles": ["admin", "researcher", "drug_chemist"]}
}
API_KEY_NAME = "X-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)

# --- Configuration for JWT ---
SECRET_KEY = "super-secret-key-replace-with-env-variable"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# --- Helper Functions for JWT (re-used from Part 2) ---
def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise JWTError("Invalid token: missing subject")
        return payload
    except JWTError as e:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Could not validate credentials: {e}")

# --- FastAPI Application Initialization ---
app = FastAPI(title="Secure Bio Vector Search API")

# --- Authentication Dependencies (re-used from Part 2 with role additions) ---
async def get_api_key_data(api_key: Annotated[str, Security(api_key_header)]) -> dict:
    if api_key not in API_KEYS:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid API Key or no API Key provided.",
            headers={"WWW-Authenticate": "Bearer"}
        )
    return API_KEYS[api_key]

async def get_current_user_payload(token: Annotated[str, Depends(oauth2_scheme)]) -> dict:
    return decode_access_token(token)

# --- Authorization Dependencies ---
class RoleChecker:
    def __init__(self, allowed_roles: list):
        self.allowed_roles = allowed_roles

    def __call__(self, user_payload: Annotated[dict, Depends(get_current_user_payload)]) -> bool:
        # Determine roles from JWT payload
        user_roles = user_payload.get("roles", []) # Assume roles are stored in a list in JWT
        if not isinstance(user_roles, list):
            user_roles = [user_roles] # Handle single role string if necessary

        # Check if user has any of the allowed roles
        if any(role in self.allowed_roles for role in user_roles):
            return True
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=f"User lacks required role(s): {', '.join(self.allowed_roles)}"
        )

class APIKeyRoleChecker:
    def __init__(self, allowed_roles: list):
        self.allowed_roles = allowed_roles

    def __call__(self, api_key_data: Annotated[dict, Depends(get_api_key_data)]) -> bool:
        api_key_roles = api_key_data.get("roles", [])
        if not isinstance(api_key_roles, list):
            api_key_roles = [api_key_roles]

        if any(role in self.allowed_roles for role in api_key_roles):
            return True
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=f"API Key lacks required role(s): {', '.join(self.allowed_roles)}"
        )

# --- Token Endpoint for JWT ---
@app.post("/token", summary="Generate an access token for user authentication")
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> dict:
    # In a real app, validate username/password against a secure user database
    # For this example, we'll use dummy credentials and assign roles.
    if form_data.username == "bio_researcher" and form_data.password == "research_pw":
        roles = ["researcher"]
    elif form_data.username == "bio_chemist" and form_data.password == "chemist_pw":
        roles = ["drug_chemist"]
    elif form_data.username == "admin_user" and form_data.password == "admin_pw":
        roles = ["admin", "researcher", "drug_chemist"]
    else:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": form_data.username, "roles": roles},
        expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer", "user_roles": roles}

# --- API Endpoints with Authorization ---
@app.get("/search/molecular", summary="Search molecular embeddings (Researcher or Admin access)")
async def search_molecular_embeddings(
    query_vector: str, top_k: int = 5,
    is_authorized_by_jwt: Annotated[bool, Depends(RoleChecker(["researcher", "admin"]))],
    is_authorized_by_api_key: Annotated[bool, Depends(APIKeyRoleChecker(["researcher", "admin"]))]
):
    # If either dependency passed, it means authentication and authorization were successful.
    # The actual execution doesn't need to know *how* it was authorized, just that it was.
    print("Authorized access to molecular embeddings search.")
    return {"status": "success", "message": "Molecular embeddings search executed with appropriate authorization.", "results": []}

@app.get("/search/protein", summary="Search protein embeddings (Drug Chemist or Admin access)")
async def search_protein_embeddings(
    query_vector: str, top_k: int = 5,
    is_authorized_by_jwt: Annotated[bool, Depends(RoleChecker(["drug_chemist", "admin"]))],
    is_authorized_by_api_key: Annotated[bool, Depends(APIKeyRoleChecker(["drug_chemist", "admin"]))]
):
    print("Authorized access to protein embeddings search.")
    return {"status": "success", "message": "Protein embeddings search executed with appropriate authorization.", "results": []}

@app.post("/ingest/genomic", summary="Ingest new genomic data (Admin only)")
async def ingest_genomic_data(
    data: dict,
    is_authorized_by_jwt: Annotated[bool, Depends(RoleChecker(["admin"]))],
    is_authorized_by_api_key: Annotated[bool, Depends(APIKeyRoleChecker(["admin"]))]
):
    print("Authorized access to genomic data ingestion.")
    return {"status": "success", "message": "Genomic data ingestion permitted."}

# --- Run the FastAPI application ---
# To run this application:
# 1. Save the code as 'main.py'.
# 2. Install FastAPI, Uvicorn, and python-jose: pip install "fastapi[all]" python-jose[cryptography]
# 3. Run from your terminal: uvicorn main:app --reload
#    Then access your API at http://127.0.0.1:8000/docs

Fortify the Deployment: Advanced Safeguards for Production-Ready Bio-APIs

Securing a biological vector search API extends beyond mere authentication and authorization; it demands a comprehensive strategy for deployment and operational resilience. We must anticipate and mitigate real-world threats, transforming our API into a robust, production-ready system. This final phase activates advanced safeguards, ensuring data integrity, availability, and sustained protection for our invaluable bioinformatics assets.


A critical step involves secure configuration management. Hardcoding sensitive information like API keys or JWT secrets is a critical vulnerability. We mandate the use of environment variables or dedicated secret management services (e.g., AWS Secrets Manager, Google Cloud Secret Manager, HashiCorp Vault). This approach prevents sensitive data from being committed to version control systems and allows dynamic updates without code changes, a cornerstone of operational security. Furthermore, implementing rate limiting is indispensable. Uncontrolled access can lead to denial-of-service attacks, resource exhaustion, or brute-force attempts against authentication endpoints. We deploy middleware to restrict the number of requests a single client can make within a given timeframe, effectively throttling malicious traffic while maintaining legitimate user experience.


Beyond these immediate implementations, we advocate for several critical best practices: HTTPS/TLS encryption is non-negotiable for all API traffic, protecting data in transit from eavesdropping. Input validation at every endpoint prevents injection attacks and ensures data integrity. Comprehensive logging and monitoring establish visibility into API activity, enabling rapid detection of suspicious patterns or breaches. We design logs to capture relevant security events (failed logins, unauthorized access attempts) without exposing sensitive data. Finally, regular security audits and penetration testing are essential; they uncover unforeseen vulnerabilities, transforming our security posture from reactive to proactively adaptive. By integrating these advanced safeguards, we fortify our bio-engineering API, establishing an unyielding defense against the evolving threat landscape.

import uvicorn
from fastapi import FastAPI, Depends, HTTPException, status, Request
from fastapi.responses import JSONResponse
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer, OAuth2PasswordRequestForm
from typing import Annotated
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
import os # For environment variables
from dotenv import load_dotenv # Optional: for loading .env files

# Load environment variables (optional, good for local dev)
load_dotenv()

# --- Configuration for API Keys (now from env) ---
API_KEYS_CONFIG = {
    os.getenv("RESEARCH_API_KEY", "default_research_key"): {"scope": "molecular_read", "team": "research", "roles": ["researcher"]},
    os.getenv("DRUG_DISCOVERY_API_KEY", "default_drug_key"): {"scope": "protein_read", "team": "drug_discovery", "roles": ["drug_chemist"]},
    os.getenv("ADMIN_API_KEY", "default_admin_key"): {"scope": "full_access", "team": "admin", "roles": ["admin", "researcher", "drug_chemist"]}
}
API_KEY_NAME = "X-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)

# --- Configuration for JWT (now from env) ---
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "super-secret-key-replace-with-env-variable")
ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("JWT_EXPIRE_MINUTES", 30))
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# --- Helper Functions for JWT (re-used from Part 2) ---
def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise JWTError("Invalid token: missing subject")
        return payload
    except JWTError as e:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Could not validate credentials: {e}")

# --- FastAPI Application Initialization ---
app = FastAPI(title="Secure Bio Vector Search API")

# --- Authentication Dependencies (re-used from Part 3 with env vars) ---
async def get_api_key_data(api_key: Annotated[str, Security(api_key_header)]) -> dict:
    if api_key not in API_KEYS_CONFIG:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid API Key or no API Key provided.",
            headers={"WWW-Authenticate": "Bearer"}
        )
    return API_KEYS_CONFIG[api_key]

async def get_current_user_payload(token: Annotated[str, Depends(oauth2_scheme)]) -> dict:
    return decode_access_token(token)

# --- Authorization Dependencies (re-used from Part 3) ---
class RoleChecker:
    def __init__(self, allowed_roles: list):
        self.allowed_roles = allowed_roles

    def __call__(self, user_payload: Annotated[dict, Depends(get_current_user_payload)]) -> bool:
        user_roles = user_payload.get("roles", [])
        if not isinstance(user_roles, list):
            user_roles = [user_roles]

        if any(role in self.allowed_roles for role in user_roles):
            return True
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=f"User lacks required role(s): {', '.join(self.allowed_roles)}"
        )

class APIKeyRoleChecker:
    def __init__(self, allowed_roles: list):
        self.allowed_roles = allowed_roles

    def __call__(self, api_key_data: Annotated[dict, Depends(get_api_key_data)]) -> bool:
        api_key_roles = api_key_data.get("roles", [])
        if not isinstance(api_key_roles, list):
            api_key_roles = [api_key_roles]

        if any(role in self.allowed_roles for role in api_key_roles):
            return True
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=f"API Key lacks required role(s): {', '.join(self.allowed_roles)}"
        )

# --- Rate Limiting Middleware ---
# In a real production environment, use a dedicated rate-limiting solution
# like Redis-backed libraries (e.g., `fastapi-limiter`) or an API Gateway.
# This is a basic in-memory example.

from collections import defaultdict
import time

REQUEST_COUNTS = defaultdict(lambda: {"count": 0, "last_reset": time.time()})
MAX_REQUESTS_PER_MINUTE = 60 # Example: 60 requests per minute

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    client_ip = request.client.host
    current_time = time.time()

    if current_time - REQUEST_COUNTS[client_ip]["last_reset"] > 60:
        REQUEST_COUNTS[client_ip]["count"] = 0
        REQUEST_COUNTS[client_ip]["last_reset"] = current_time

    if REQUEST_COUNTS[client_ip]["count"] >= MAX_REQUESTS_PER_MINUTE:
        return JSONResponse(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            content={"detail": "Rate limit exceeded. Try again later.", "retry_after": 60 - (current_time - REQUEST_COUNTS[client_ip]["last_reset"])}
        )

    REQUEST_COUNTS[client_ip]["count"] += 1
    response = await call_next(request)
    return response

# --- Token Endpoint for JWT (re-used from Part 3) ---
@app.post("/token", summary="Generate an access token for user authentication")
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> dict:
    # In a real app, validate username/password against a secure user database
    # For this example, we'll use dummy credentials and assign roles.
    if form_data.username == "bio_researcher" and form_data.password == "research_pw":
        roles = ["researcher"]
    elif form_data.username == "bio_chemist" and form_data.password == "chemist_pw":
        roles = ["drug_chemist"]
    elif form_data.username == "admin_user" and form_data.password == "admin_pw":
        roles = ["admin", "researcher", "drug_chemist"]
    else:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": form_data.username, "roles": roles},
        expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer", "user_roles": roles}

# --- API Endpoints with Authorization (re-used from Part 3) ---
@app.get("/search/molecular", summary="Search molecular embeddings (Researcher or Admin access)")
async def search_molecular_embeddings(
    query_vector: str, top_k: int = 5,
    is_authorized_by_jwt: Annotated[bool, Depends(RoleChecker(["researcher", "admin"]))],
    is_authorized_by_api_key: Annotated[bool, Depends(APIKeyRoleChecker(["researcher", "admin"]))]
):
    print("Authorized access to molecular embeddings search.")
    return {"status": "success", "message": "Molecular embeddings search executed with appropriate authorization.", "results": []}

@app.get("/search/protein", summary="Search protein embeddings (Drug Chemist or Admin access)")
async def search_protein_embeddings(
    query_vector: str, top_k: int = 5,
    is_authorized_by_jwt: Annotated[bool, Depends(RoleChecker(["drug_chemist", "admin"]))],
    is_authorized_by_api_key: Annotated[bool, Depends(APIKeyRoleChecker(["drug_chemist", "admin"]))]
):
    print("Authorized access to protein embeddings search.")
    return {"status": "success", "message": "Protein embeddings search executed with appropriate authorization.", "results": []}

@app.post("/ingest/genomic", summary="Ingest new genomic data (Admin only)")
async def ingest_genomic_data(
    data: dict,
    is_authorized_by_jwt: Annotated[bool, Depends(RoleChecker(["admin"]))],
    is_authorized_by_api_key: Annotated[bool, Depends(APIKeyRoleChecker(["admin"]))]
):
    print("Authorized access to genomic data ingestion.")
    return {"status": "success", "message": "Genomic data ingestion permitted."}

# --- Run the FastAPI application ---
# To run this application:
# 1. Save the code as 'main.py'.
# 2. Create a '.env' file in the same directory with your actual secret keys, e.g.:
#    JWT_SECRET_KEY="your_very_strong_jwt_secret"
#    RESEARCH_API_KEY="your_research_api_key"
#    DRUG_DISCOVERY_API_KEY="your_drug_discovery_api_key"
#    ADMIN_API_KEY="your_admin_api_key"
#    JWT_EXPIRE_MINUTES=60
# 3. Install FastAPI, Uvicorn, python-jose, and python-dotenv: pip install "fastapi[all]" python-jose[cryptography] python-dotenv
# 4. Run from your terminal: uvicorn main:app --reload
#    Then access your API at http://127.0.0.1:8000/docs

Key Takeaways

Foundational Security for Bio-APIs

Activate Foundational Security: We architect secure vector search APIs for biological data by integrating security as a core design principle. FastAPI's asynchronous capabilities and dependency injection empower us to build high-performance, secure systems. Initial implementation leverages API Key authentication, defining valid keys and a dependency to validate them, raising a 401 Unauthorized error for invalid access. This proactive defense forms the bedrock for safeguarding sensitive molecular and protein embeddings.

Robust Authentication for Biological Insights

Implement Robust Authentication: We enhance security by evolving API keys with contextual metadata (scopes, teams) and introducing JSON Web Tokens (JWT) for more dynamic scenarios. JWTs encapsulate identity and claims, offering stateless authentication with expiry. We establish a /token endpoint for issuing JWTs and refine dependencies to handle both intelligent API keys and JWTs. This dual-pronged approach allows flexible, robust authentication for varied client interactions with biological data.

Fine-Grained Access Control (RBAC)

Forge Fine-Grained Access Control: We implement Role-Based Access Control (RBAC) to precisely manage what an authenticated entity can do. Roles (e.g., 'researcher', 'drug_chemist', 'admin') are assigned to users or API keys, dictating access to specific API endpoints and data types. Custom FastAPI RoleChecker dependencies evaluate these roles, raising 403 Forbidden errors for unauthorized actions. This strategy ensures granular protection, preventing unintended data exposure or manipulation within biological pipelines.

Deployment Safeguards & Best Practices

Fortify the Deployment: We prepare our secure API for production with advanced safeguards. This includes using environment variables or secret managers for sensitive configurations to prevent hardcoding. Rate limiting middleware protects against abuse and DoS attacks. Essential best practices cover HTTPS/TLS encryption, rigorous input validation, comprehensive logging and monitoring, and regular security audits. These measures ensure operational resilience and continuous protection for critical bioinformatics assets.

FAQ

  • Why is authentication crucial for biological vector search APIs?

    Authentication is crucial because biological data (e.g., molecular structures, protein sequences, genomic information) is highly sensitive, proprietary, and often subject to strict regulatory compliance. Unauthenticated access risks data breaches, intellectual property theft, research compromise, and can severely impact drug discovery or personalized medicine initiatives. It ensures only authorized entities interact with valuable biological insights.
  • What are the common authentication methods suitable for Python-based vector search APIs?

    Common methods include API Keys for straightforward client identification (often for machine-to-machine or internal services), JSON Web Tokens (JWT) for stateless authentication with claims (suitable for web/mobile apps and user sessions), and OAuth 2.0 for delegated authorization (ideal for third-party integrations). Each method offers distinct advantages depending on the specific use case and security requirements.
  • How does Role-Based Access Control (RBAC) enhance security for biological data?

    RBAC enhances security by assigning specific roles (e.g., 'researcher', 'drug_chemist', 'admin') to authenticated users or API keys. These roles are then granted permissions to access certain endpoints or data types (e.g., 'researcher' can read molecular embeddings, 'drug_chemist' can read protein embeddings, 'admin' has full access). This prevents unauthorized entities from accessing or manipulating sensitive biological data beyond their designated scope, ensuring granular control and compliance.
  • What are key best practices for deploying a secure vector search API in production?

    Key practices include: HTTPS/TLS encryption for all communications; using environment variables or secret managers for sensitive configurations; implementing rate limiting to prevent abuse and DoS attacks; rigorous input validation to guard against injection attacks; comprehensive logging and monitoring for threat detection; and regular security audits and penetration testing to identify and fix vulnerabilities before they are exploited. These measures collectively establish a robust defense.
  • Can I use both API Key and JWT authentication in the same FastAPI application?

    Yes, FastAPI's dependency injection system allows you to define multiple authentication dependencies and apply them to endpoints. You can configure an endpoint to accept either a valid API key OR a valid JWT, or even require both for extremely sensitive operations. This provides flexibility, enabling different client types (e.g., automated scripts vs. user-facing applications) to interact with your API securely while maintaining specific access controls.