mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-25 07:12:20 +01:00
Squash Odysseus development history
This commit is contained in:
+123
-5
@@ -12,6 +12,65 @@ from src.auth_helpers import get_current_user
|
||||
|
||||
MAX_NAME_LEN = 100
|
||||
DEFAULT_SCOPES = "chat"
|
||||
ALLOWED_SCOPES = {
|
||||
"chat",
|
||||
"todos:read",
|
||||
"todos:write",
|
||||
"documents:read",
|
||||
"documents:write",
|
||||
"email:read",
|
||||
"email:draft",
|
||||
"email:send",
|
||||
"calendar:read",
|
||||
"calendar:write",
|
||||
"memory:read",
|
||||
"memory:write",
|
||||
"cookbook:read",
|
||||
"cookbook:launch",
|
||||
}
|
||||
TOKEN_PROFILES = {
|
||||
"chat": ["chat"],
|
||||
"codex_todos": ["todos:read", "todos:write"],
|
||||
"codex_documents": ["documents:read", "documents:write"],
|
||||
"codex_email_drafts": ["email:read", "email:draft", "documents:read", "documents:write"],
|
||||
}
|
||||
|
||||
|
||||
def _normalize_scopes(scopes: str | list[str] | None = None, profile: str | None = None) -> list[str]:
|
||||
profile = profile if isinstance(profile, str) else None
|
||||
profile_key = (profile or "").strip()
|
||||
if profile_key:
|
||||
if profile_key not in TOKEN_PROFILES:
|
||||
raise HTTPException(400, "Unknown token profile")
|
||||
requested = list(TOKEN_PROFILES[profile_key])
|
||||
elif isinstance(scopes, list):
|
||||
requested = [str(s).strip() for s in scopes if str(s).strip()]
|
||||
elif isinstance(scopes, str) and scopes:
|
||||
requested = [s.strip() for s in scopes.replace(" ", ",").split(",") if s.strip()]
|
||||
else:
|
||||
requested = [DEFAULT_SCOPES]
|
||||
|
||||
normalized = []
|
||||
for scope in requested:
|
||||
if scope not in ALLOWED_SCOPES:
|
||||
raise HTTPException(400, f"Unknown token scope: {scope}")
|
||||
if scope not in normalized:
|
||||
normalized.append(scope)
|
||||
|
||||
def ensure_before(write_scope: str, read_scope: str):
|
||||
if write_scope not in normalized or read_scope in normalized:
|
||||
return
|
||||
idx = normalized.index(write_scope)
|
||||
normalized.insert(idx, read_scope)
|
||||
|
||||
ensure_before("todos:write", "todos:read")
|
||||
ensure_before("documents:write", "documents:read")
|
||||
ensure_before("calendar:write", "calendar:read")
|
||||
ensure_before("memory:write", "memory:read")
|
||||
ensure_before("email:draft", "email:read")
|
||||
ensure_before("cookbook:launch", "cookbook:read")
|
||||
|
||||
return normalized or [DEFAULT_SCOPES]
|
||||
|
||||
|
||||
def setup_api_token_routes() -> APIRouter:
|
||||
@@ -45,13 +104,28 @@ def setup_api_token_routes() -> APIRouter:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@router.get("/tokens/profiles")
|
||||
def token_profiles(request: Request):
|
||||
require_admin(request)
|
||||
return {
|
||||
"profiles": TOKEN_PROFILES,
|
||||
"allowed_scopes": sorted(ALLOWED_SCOPES),
|
||||
}
|
||||
|
||||
@router.post("/tokens")
|
||||
def create_token(request: Request, name: str = Form("")):
|
||||
def create_token(
|
||||
request: Request,
|
||||
name: str = Form(""),
|
||||
scopes: str = Form(None),
|
||||
profile: str = Form(None),
|
||||
):
|
||||
require_admin(request)
|
||||
name = name.strip()[:MAX_NAME_LEN]
|
||||
if not name:
|
||||
raise HTTPException(400, "Token name is required")
|
||||
owner = get_current_user(request)
|
||||
scope_list = _normalize_scopes(scopes, profile)
|
||||
scopes_value = ",".join(scope_list)
|
||||
|
||||
raw_token = "ody_" + secrets.token_urlsafe(32)
|
||||
token_hash = bcrypt.hashpw(raw_token.encode(), bcrypt.gensalt()).decode()
|
||||
@@ -64,7 +138,7 @@ def setup_api_token_routes() -> APIRouter:
|
||||
name=name,
|
||||
token_hash=token_hash,
|
||||
token_prefix=raw_token[:8],
|
||||
scopes=DEFAULT_SCOPES,
|
||||
scopes=scopes_value,
|
||||
is_active=True,
|
||||
))
|
||||
_invalidate_cache(request)
|
||||
@@ -75,16 +149,60 @@ def setup_api_token_routes() -> APIRouter:
|
||||
"owner": owner,
|
||||
"token": raw_token,
|
||||
"token_prefix": raw_token[:8],
|
||||
"scopes": DEFAULT_SCOPES.split(","),
|
||||
"scopes": scope_list,
|
||||
}
|
||||
|
||||
@router.patch("/tokens/{token_id}")
|
||||
async def update_token(request: Request, token_id: str):
|
||||
require_admin(request)
|
||||
current_user = get_current_user(request)
|
||||
try:
|
||||
payload = await request.json()
|
||||
except Exception:
|
||||
payload = {}
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
with get_db_session() as db:
|
||||
token = db.query(ApiToken).filter(ApiToken.id == token_id).first()
|
||||
if not token:
|
||||
raise HTTPException(404, "Token not found")
|
||||
if current_user and token.owner != current_user:
|
||||
raise HTTPException(403, "Not your token")
|
||||
if isinstance(payload.get("name"), str) and payload["name"].strip():
|
||||
token.name = payload["name"].strip()[:MAX_NAME_LEN]
|
||||
# Only touch scopes when the caller actually sent them. A partial
|
||||
# update such as a rename ({"name": ...} with no "scopes" key) must
|
||||
# not silently reset the token to the default scope — that dropped
|
||||
# every previously granted scope.
|
||||
if "scopes" in payload:
|
||||
token.scopes = ",".join(_normalize_scopes(payload.get("scopes")))
|
||||
db.add(token)
|
||||
current_scopes = [
|
||||
s.strip()
|
||||
for s in (getattr(token, "scopes", "") or DEFAULT_SCOPES).split(",")
|
||||
if s.strip()
|
||||
]
|
||||
response = {
|
||||
"id": token_id,
|
||||
"name": getattr(token, "name", ""),
|
||||
"owner": getattr(token, "owner", None),
|
||||
"token_prefix": getattr(token, "token_prefix", ""),
|
||||
"scopes": current_scopes,
|
||||
}
|
||||
_invalidate_cache(request)
|
||||
return response
|
||||
|
||||
@router.delete("/tokens/{token_id}")
|
||||
def delete_token(request: Request, token_id: str):
|
||||
require_admin(request)
|
||||
current_user = get_current_user(request)
|
||||
with get_db_session() as db:
|
||||
deleted = db.query(ApiToken).filter(ApiToken.id == token_id).delete()
|
||||
if not deleted:
|
||||
token = db.query(ApiToken).filter(ApiToken.id == token_id).first()
|
||||
if not token:
|
||||
raise HTTPException(404, "Token not found")
|
||||
if current_user and token.owner != current_user:
|
||||
raise HTTPException(403, "Not your token")
|
||||
db.delete(token)
|
||||
_invalidate_cache(request)
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user