basyx commited on
Commit
c8a0391
·
verified ·
1 Parent(s): 5bceb55

Enforce role-limited Studio authorization

Browse files

Extract role-scope intersection so membership authorization can be regression tested.

Files changed (1) hide show
  1. app/security/studio_identity.py +32 -1
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
+ )