mirror of
https://github.com/apidoorman/doorman.git
synced 2025-12-19 00:19:38 -06:00
API credit key rotation
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -80,3 +80,4 @@ backend-services/routes/proto/*
|
||||
.production.env
|
||||
.coverage
|
||||
coverage_html/
|
||||
doorman.code-workspace
|
||||
|
||||
@@ -19,6 +19,7 @@ from utils.auth_blacklist import TimedHeap, jwt_blacklist, revoke_all_for_user,
|
||||
from utils.role_util import platform_role_required_bool
|
||||
from utils.role_util import is_admin_user
|
||||
from models.update_user_model import UpdateUserModel
|
||||
from models.create_user_model import CreateUserModel
|
||||
from utils.limit_throttle_util import limit_by_ip
|
||||
|
||||
authorization_router = APIRouter()
|
||||
@@ -220,6 +221,92 @@ async def authorization(request: Request):
|
||||
end_time = time.time() * 1000
|
||||
logger.info(f'{request_id} | Total time: {str(end_time - start_time)}ms')
|
||||
|
||||
"""
|
||||
Register new user
|
||||
|
||||
Request:
|
||||
{}
|
||||
Response:
|
||||
{}
|
||||
"""
|
||||
|
||||
@authorization_router.post('/authorization/register',
|
||||
description='Register new user',
|
||||
response_model=ResponseModel,
|
||||
responses={
|
||||
200: {
|
||||
'description': 'Successful Response',
|
||||
'content': {
|
||||
'application/json': {
|
||||
'example': {
|
||||
'message': 'User created successfully'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async def register(request: Request):
|
||||
request_id = str(uuid.uuid4())
|
||||
start_time = time.time() * 1000
|
||||
try:
|
||||
# Rate limit registration to prevent abuse
|
||||
reg_limit = int(os.getenv('REGISTER_IP_RATE_LIMIT', '5'))
|
||||
reg_window = int(os.getenv('REGISTER_IP_RATE_WINDOW', '3600'))
|
||||
rate_limit_info = await limit_by_ip(request, limit=reg_limit, window=reg_window)
|
||||
|
||||
logger.info(f'{request_id} | Register request from: {request.client.host}:{request.client.port}')
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return respond_rest(ResponseModel(
|
||||
status_code=400,
|
||||
response_headers={'request_id': request_id},
|
||||
error_code='AUTH004',
|
||||
error_message='Invalid JSON payload'
|
||||
))
|
||||
|
||||
# Validate required fields
|
||||
if not data.get('email') or not data.get('password'):
|
||||
return respond_rest(ResponseModel(
|
||||
status_code=400,
|
||||
response_headers={'request_id': request_id},
|
||||
error_code='AUTH001',
|
||||
error_message='Missing email or password'
|
||||
))
|
||||
|
||||
# Create user model
|
||||
# Default to 'user' role and active=True
|
||||
user_data = CreateUserModel(
|
||||
username=data.get('email').split('@')[0], # Simple username derivation
|
||||
email=data.get('email'),
|
||||
password=data.get('password'),
|
||||
role='user',
|
||||
active=True
|
||||
)
|
||||
|
||||
# Check if user exists (UserService.create_user handles this but we want clean error)
|
||||
# Actually UserService.create_user will return error if exists.
|
||||
|
||||
result = await UserService.create_user(user_data, request_id)
|
||||
|
||||
# If successful, we could auto-login, but for now just return success
|
||||
return respond_rest(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f'{request_id} | Unexpected error: {str(e)}', exc_info=True)
|
||||
return respond_rest(ResponseModel(
|
||||
status_code=500,
|
||||
response_headers={'request_id': request_id},
|
||||
error_code='GTW999',
|
||||
error_message='An unexpected error occurred'
|
||||
))
|
||||
finally:
|
||||
end_time = time.time() * 1000
|
||||
logger.info(f'{request_id} | Total time: {str(end_time - start_time)}ms')
|
||||
|
||||
"""
|
||||
Endpoint
|
||||
|
||||
|
||||
@@ -424,3 +424,71 @@ async def get_credits(username: str, request: Request):
|
||||
finally:
|
||||
end_time = time.time() * 1000
|
||||
logger.info(f'{request_id} | Total time: {str(end_time - start_time)}ms')
|
||||
|
||||
"""
|
||||
Rotate API key for a user
|
||||
|
||||
Request:
|
||||
{}
|
||||
Response:
|
||||
{}
|
||||
"""
|
||||
|
||||
@credit_router.post('/rotate-key',
|
||||
description='Rotate API key for the authenticated user',
|
||||
response_model=ResponseModel,
|
||||
responses={
|
||||
200: {
|
||||
'description': 'Successful Response',
|
||||
'content': {
|
||||
'application/json': {
|
||||
'example': {
|
||||
'api_key': '******************'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async def rotate_key(request: Request):
|
||||
request_id = str(uuid.uuid4())
|
||||
start_time = time.time() * 1000
|
||||
try:
|
||||
payload = await auth_required(request)
|
||||
username = payload.get('sub')
|
||||
|
||||
# Get group from body
|
||||
try:
|
||||
body = await request.json()
|
||||
group = body.get('api_credit_group')
|
||||
except Exception:
|
||||
group = None
|
||||
|
||||
if not group:
|
||||
return respond_rest(ResponseModel(
|
||||
status_code=400,
|
||||
error_code='CRD020',
|
||||
error_message='api_credit_group is required'
|
||||
))
|
||||
|
||||
logger.info(f'{request_id} | Username: {username} | From: {request.client.host}:{request.client.port}')
|
||||
logger.info(f'{request_id} | Endpoint: {request.method} {str(request.url.path)}')
|
||||
|
||||
# No special role required, just authentication
|
||||
|
||||
return respond_rest(await CreditService.rotate_api_key(username, group, request_id))
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f'{request_id} | Unexpected error: {str(e)}', exc_info=True)
|
||||
return respond_rest(ResponseModel(
|
||||
status_code=500,
|
||||
response_headers={
|
||||
'request_id': request_id
|
||||
},
|
||||
error_code='GTW999',
|
||||
error_message='An unexpected error occurred'
|
||||
))
|
||||
finally:
|
||||
end_time = time.time() * 1000
|
||||
logger.info(f'{request_id} | Total time: {str(end_time - start_time)}ms')
|
||||
|
||||
@@ -7,6 +7,7 @@ See https://github.com/apidoorman/doorman for more information
|
||||
from pymongo.errors import PyMongoError
|
||||
import logging
|
||||
from typing import Optional
|
||||
import secrets
|
||||
|
||||
from models.response_model import ResponseModel
|
||||
from models.credit_model import CreditModel
|
||||
@@ -299,3 +300,43 @@ class CreditService:
|
||||
except PyMongoError as e:
|
||||
logger.error(request_id + f' | Get user credits failed with database error: {str(e)}')
|
||||
return ResponseModel(status_code=500, error_code='CRD018', error_message='Database error occurred while retrieving user credits').dict()
|
||||
|
||||
@staticmethod
|
||||
async def rotate_api_key(username: str, group: str, request_id):
|
||||
logger.info(request_id + f' | Rotating API key for user: {username}, group: {group}')
|
||||
try:
|
||||
doc = await db_find_one(user_credit_collection, {'username': username})
|
||||
if not doc:
|
||||
# Create if not exists? Or error?
|
||||
# For rotation, we expect it to exist, or at least the user to exist.
|
||||
# But maybe we can create the credit entry if it's missing.
|
||||
# Let's error if not found for now, or create empty.
|
||||
doc = {'username': username, 'users_credits': {}}
|
||||
|
||||
users_credits = doc.get('users_credits') or {}
|
||||
group_credits = users_credits.get(group) or {}
|
||||
|
||||
# Generate new key
|
||||
new_key = secrets.token_urlsafe(32)
|
||||
encrypted_key = encrypt_value(new_key)
|
||||
|
||||
# Update group credits
|
||||
# Preserve other fields in group_credits (like available_credits)
|
||||
if isinstance(group_credits, dict):
|
||||
group_credits['user_api_key'] = encrypted_key
|
||||
else:
|
||||
# Should be a dict, but if it was somehow not, reset it
|
||||
group_credits = {'user_api_key': encrypted_key, 'available_credits': 0, 'tier_name': 'default'}
|
||||
|
||||
users_credits[group] = group_credits
|
||||
|
||||
if doc.get('_id'):
|
||||
await db_update_one(user_credit_collection, {'username': username}, {'$set': {'users_credits': users_credits}})
|
||||
else:
|
||||
await db_insert_one(user_credit_collection, {'username': username, 'users_credits': users_credits})
|
||||
|
||||
return ResponseModel(status_code=200, response={'api_key': new_key}).dict()
|
||||
|
||||
except PyMongoError as e:
|
||||
logger.error(request_id + f' | Rotate key failed with database error: {str(e)}')
|
||||
return ResponseModel(status_code=500, error_code='CRD019', error_message='Database error occurred while rotating API key').dict()
|
||||
|
||||
@@ -490,6 +490,9 @@ class GatewayService:
|
||||
allowed_headers = api.get('api_allowed_headers') or [] if api else []
|
||||
headers = await get_headers(request, allowed_headers)
|
||||
headers['X-Request-ID'] = request_id
|
||||
if username:
|
||||
headers['X-User-Email'] = str(username)
|
||||
headers['X-Doorman-User'] = str(username)
|
||||
if api and api.get('api_credits_enabled'):
|
||||
ai_token_headers = await credit_util.get_credit_api_header(api.get('api_credit_group'))
|
||||
if ai_token_headers:
|
||||
|
||||
Reference in New Issue
Block a user