Skip to content

API Keys Should Be User-Specific Instead of Globally Shared #29

@nehmetohmedb

Description

@nehmetohmedb

Problem Description

Currently, API keys stored in the apikey table are globally shared across all users and groups in the system. This poses security and privacy concerns as:

  1. Security Risk: Any user can see and potentially use API keys added by other users
  2. Privacy Concern: Users cannot have personal API keys for their own usage
  3. Billing Issues: API usage from shared keys cannot be attributed to specific users
  4. Compliance: In multi-tenant environments, sharing API keys across organizations violates data isolation principles

Current Implementation

The ApiKey model (src/backend/src/models/api_key.py) lacks user or group association:

class ApiKey(Base):
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=False, unique=True, index=True)
    encrypted_value = Column(String, nullable=False)
    description = Column(String, nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

Proposed Solution

Option 1: User-Specific API Keys (Recommended)

Add user_id to the ApiKey model to make keys personal to each user:

class ApiKey(Base):
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=False, index=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
    encrypted_value = Column(String, nullable=False)
    description = Column(String, nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Relationship
    user = relationship("User", back_populates="api_keys")
    
    # Unique constraint on (name, user_id) instead of just name
    __table_args__ = (
        UniqueConstraint('name', 'user_id', name='uq_apikey_name_user'),
    )

Option 2: Group-Scoped API Keys

Follow the existing pattern and add group_id to make keys accessible within groups:

class ApiKey(Base):
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=False, index=True)
    group_id = Column(String, ForeignKey("groups.id"), nullable=False, index=True)
    encrypted_value = Column(String, nullable=False)
    description = Column(String, nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Relationship
    group = relationship("Group", back_populates="api_keys")
    
    # Unique constraint on (name, group_id)
    __table_args__ = (
        UniqueConstraint('name', 'group_id', name='uq_apikey_name_group'),
    )

Option 3: Hybrid Approach

Support both user-specific and group-shared keys:

class ApiKey(Base):
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=False, index=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
    group_id = Column(String, ForeignKey("groups.id"), nullable=True, index=True)
    scope = Column(Enum(ApiKeyScope), nullable=False, default=ApiKeyScope.USER)  # USER or GROUP
    encrypted_value = Column(String, nullable=False)
    description = Column(String, nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Check constraint: either user_id or group_id must be set
    __table_args__ = (
        CheckConstraint('(user_id IS NOT NULL AND group_id IS NULL AND scope = "USER") OR (user_id IS NULL AND group_id IS NOT NULL AND scope = "GROUP")', name='check_apikey_scope'),
        UniqueConstraint('name', 'user_id', 'group_id', name='uq_apikey_name_scope'),
    )

Implementation Requirements

  1. Database Migration: Create Alembic migration to add user_id/group_id columns
  2. API Updates:
    • Update API endpoints to filter keys by current user/group
    • Add user context to API key router using UserDep or GroupContextDep
    • Update service layer to handle user/group filtering
  3. Frontend Updates:
    • No changes needed if API contract remains the same
    • Consider adding UI indicators for personal vs shared keys (if hybrid approach)
  4. Data Migration:
    • Existing keys could be assigned to a system user or the first admin user
    • Or require users to re-enter their API keys
  5. Testing: Update tests to verify proper isolation

Benefits

  1. Security: Users can only access their own API keys
  2. Privacy: Personal API keys remain private
  3. Accountability: API usage can be tracked per user
  4. Flexibility: Users can have different keys for different purposes
  5. Consistency: Aligns with the group isolation pattern used throughout the system

Considerations

  • The memory_backend model is already user-specific, showing precedent for user-scoped resources
  • Most other entities use group_id for multi-tenant isolation
  • Need to decide if API keys should be shareable within groups or strictly personal
  • Consider backward compatibility for existing API keys

Recommendation

I recommend Option 1 (User-Specific) as API keys are typically personal credentials that shouldn't be shared. This aligns with security best practices and how most platforms handle API keys.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions