feat: intégration TikTak #197-200 + scoped tokens, webhooks filtrés, listes mtime/size ; fix e2e-server BUG-112
CI / lint (push) Successful in 3m17s
CI / security (push) Failing after 1m51s
CI / test (push) Successful in 5m14s
CI / build (push) Successful in 1m50s
CI / e2e (push) Successful in 18m18s

This commit is contained in:
2026-10-14 11:42:53 -04:00
parent 2be0b15bcf
commit f783394f09
59 changed files with 3233 additions and 77 deletions
+37 -7
View File
@@ -215,15 +215,37 @@ def _save_api_tokens(data: dict):
tmp.replace(API_TOKENS_FILE)
def create_api_token(user: dict, name: str, expiry_key: str) -> tuple[dict, str]:
#: Endpoint perimeters accepted for scoped API tokens (#199). ``"full"``
#: keeps today's behaviour ; ``"files"`` restricts the token to the file
#: read/write surface (enforced by ``TokenScopeMiddleware`` on REST and by
#: the tool registry on MCP/agent calls).
API_TOKEN_SCOPES = ("full", "files")
def create_api_token(
user: dict,
name: str,
expiry_key: str,
vaults: list[str] | None = None,
scope: str = "full",
) -> tuple[dict, str]:
"""Create a persistent API/MCP token. Returns (record, jwt_string).
``expiry_key`` must be one of API_TOKEN_EXPIRY_CHOICES; "never" omits the
``exp`` claim (valid until explicitly revoked).
#199 (synchro TikTak) — optional least-privilege restriction:
``vaults`` limits the token to the given vaults (``None`` = snapshot of
the creator's vaults, i.e. today's behaviour) and ``scope`` limits the
endpoint perimeter (``"full"`` or ``"files"``). Both are embedded in the
JWT so enforcement stays stateless; the record keeps them for display.
"""
if expiry_key not in API_TOKEN_EXPIRY_CHOICES:
raise ValueError("Expiration invalide")
if scope not in API_TOKEN_SCOPES:
raise ValueError("Portée invalide (full | files)")
seconds = API_TOKEN_EXPIRY_CHOICES[expiry_key]
resolved_vaults = list(vaults) if vaults is not None else list(user.get("vaults", []))
with _api_tokens_lock:
data = _load_api_tokens()
tokens = data["tokens"]
@@ -235,11 +257,12 @@ def create_api_token(user: dict, name: str, expiry_key: str) -> tuple[dict, str]
payload = {
"sub": user["username"],
"role": user.get("role", "user"),
"vaults": user.get("vaults", []),
"vaults": resolved_vaults,
"jti": jti,
"iat": now,
"type": "access",
"api": True,
"scope": scope,
}
if seconds is not None:
payload["exp"] = now + seconds
@@ -252,6 +275,8 @@ def create_api_token(user: dict, name: str, expiry_key: str) -> tuple[dict, str]
"expires_at": payload.get("exp"),
"expiry_key": expiry_key,
"last_used_at": None,
"vaults": resolved_vaults,
"scope": scope,
}
tokens[jti] = record
_save_api_tokens(data)
@@ -262,11 +287,16 @@ def list_api_tokens(username: str) -> list[dict]:
"""Token metadata for one user, newest first."""
data = _load_api_tokens()
now = int(time.time())
items = [
{**t, "expired": t.get("expires_at") is not None and t["expires_at"] < now}
for t in data["tokens"].values()
if t["username"] == username
]
items = []
for t in data["tokens"].values():
if t["username"] != username:
continue
# #199 : les clés créées avant la portée limitée n'ont ni scope ni
# vaults stockés → portée pleine, comportement inchangé.
entry = {**t, "expired": t.get("expires_at") is not None and t["expires_at"] < now}
entry.setdefault("scope", "full")
entry.setdefault("vaults", None)
items.append(entry)
return sorted(items, key=lambda t: t["created_at"], reverse=True)