Spaces:
Running
Running
Enforce role-limited Studio authorization
Browse filesExtract role-scope intersection so membership authorization can be regression tested.
app/security/studio_identity.py
CHANGED
|
@@ -4,7 +4,11 @@ import hmac
|
|
| 4 |
import re
|
| 5 |
import time
|
| 6 |
from collections.abc import Mapping
|
| 7 |
-
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
_HEX_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
| 10 |
|
|
@@ -57,3 +61,30 @@ def verify_studio_identity(
|
|
| 57 |
if not hmac.compare_digest(expected, signature):
|
| 58 |
raise ValueError("Studio identity signature is invalid.")
|
| 59 |
return StudioIdentity(subject=subject, email=email, name=name, timestamp=timestamp)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import re
|
| 5 |
import time
|
| 6 |
from collections.abc import Mapping
|
| 7 |
+
from dataclasses import dataclass, replace
|
| 8 |
+
|
| 9 |
+
from app.security.context import AuthContext
|
| 10 |
+
from app.security.scopes import DEFAULT_ROLE_SCOPES
|
| 11 |
+
from app.security.tenancy import TenantPrincipal
|
| 12 |
|
| 13 |
_HEX_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
| 14 |
|
|
|
|
| 61 |
if not hmac.compare_digest(expected, signature):
|
| 62 |
raise ValueError("Studio identity signature is invalid.")
|
| 63 |
return StudioIdentity(subject=subject, email=email, name=name, timestamp=timestamp)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def constrain_context_to_membership(
|
| 67 |
+
context: AuthContext, principal: TenantPrincipal
|
| 68 |
+
) -> AuthContext:
|
| 69 |
+
"""Intersect API-key scopes with persisted workspace-role capabilities."""
|
| 70 |
+
raw_role = principal.membership_role.strip().lower()
|
| 71 |
+
role_map = {
|
| 72 |
+
"owner": "developer",
|
| 73 |
+
"admin": "developer",
|
| 74 |
+
"developer": "developer",
|
| 75 |
+
"operator": "operator",
|
| 76 |
+
"viewer": "viewer",
|
| 77 |
+
"member": "viewer",
|
| 78 |
+
}
|
| 79 |
+
effective_role = role_map.get(raw_role)
|
| 80 |
+
if effective_role is None:
|
| 81 |
+
raise ValueError("Unsupported workspace membership role.")
|
| 82 |
+
return replace(
|
| 83 |
+
context,
|
| 84 |
+
role=effective_role,
|
| 85 |
+
scopes=frozenset(context.scopes.intersection(DEFAULT_ROLE_SCOPES[effective_role])),
|
| 86 |
+
workspace_id=principal.workspace_id,
|
| 87 |
+
user_id=principal.user_id,
|
| 88 |
+
membership_id=principal.membership_id,
|
| 89 |
+
membership_role=raw_role,
|
| 90 |
+
)
|