feat: Add role-based API tokens for Claudia Docs
- Add api_tokens table with role-based access (researcher, developer, viewer)
- Add POST /auth/token/generate endpoint for creating tokens
- Add GET /auth/tokens endpoint for listing user's tokens
- Add DELETE /auth/tokens/{token_id} endpoint for revoking tokens
- Add agent_type field to documents (research, development, general)
- Implement role-based access control for documents:
- researcher: access to research and general documents
- developer: access to development and general documents
- viewer: read-only access
- Update document model and schemas with agent_type field
- Add comprehensive tests for API token functionality
- All existing tests pass (73 total)
This commit is contained in:
@@ -231,3 +231,86 @@ async def cleanup_expired_blocklist(db: AsyncSession) -> None:
|
||||
text("DELETE FROM jwt_blocklist WHERE expires_at < datetime('now')")
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# API Token Management (Role-based)
|
||||
# =============================================================================
|
||||
|
||||
def generate_api_token() -> str:
|
||||
"""Generate a random API token."""
|
||||
return secrets.token_urlsafe(48) # ~64 chars
|
||||
|
||||
|
||||
async def create_api_token(
|
||||
db: AsyncSession,
|
||||
name: str,
|
||||
role: str,
|
||||
agent_id: str,
|
||||
) -> tuple[str, "ApiToken"]:
|
||||
"""Create a new API token. Returns (raw_token, token_record)."""
|
||||
from app.models.api_token import ApiToken
|
||||
|
||||
raw_token = generate_api_token()
|
||||
token_hash = hash_token(raw_token)
|
||||
|
||||
token_record = ApiToken(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
token_hash=token_hash,
|
||||
role=role,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
db.add(token_record)
|
||||
await db.flush()
|
||||
return raw_token, token_record
|
||||
|
||||
|
||||
async def list_api_tokens(db: AsyncSession, agent_id: str) -> list["ApiToken"]:
|
||||
"""List all API tokens for an agent (without the actual token)."""
|
||||
from app.models.api_token import ApiToken
|
||||
|
||||
result = await db.execute(
|
||||
select(ApiToken).where(ApiToken.agent_id == agent_id).order_by(ApiToken.created_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def revoke_api_token(db: AsyncSession, token_id: str, agent_id: str) -> bool:
|
||||
"""Revoke an API token. Returns True if found and deleted."""
|
||||
from app.models.api_token import ApiToken
|
||||
|
||||
result = await db.execute(
|
||||
select(ApiToken).where(
|
||||
ApiToken.id == token_id,
|
||||
ApiToken.agent_id == agent_id,
|
||||
)
|
||||
)
|
||||
token = result.scalar_one_or_none()
|
||||
if not token:
|
||||
return False
|
||||
|
||||
await db.delete(token)
|
||||
await db.flush()
|
||||
return True
|
||||
|
||||
|
||||
async def verify_api_token(db: AsyncSession, raw_token: str) -> tuple[str, str] | None:
|
||||
"""
|
||||
Verify an API token. Returns (agent_id, role) if valid, None otherwise.
|
||||
Also updates last_used_at.
|
||||
"""
|
||||
from app.models.api_token import ApiToken
|
||||
|
||||
token_hash = hash_token(raw_token)
|
||||
result = await db.execute(
|
||||
select(ApiToken).where(ApiToken.token_hash == token_hash)
|
||||
)
|
||||
token: ApiToken | None = result.scalar_one_or_none()
|
||||
if not token:
|
||||
return None
|
||||
|
||||
# Update last_used_at
|
||||
token.last_used_at = datetime.utcnow()
|
||||
await db.flush()
|
||||
return token.agent_id, token.role
|
||||
|
||||
Reference in New Issue
Block a user