Spaces:
Running
Running
Map verified Studio subjects to isolated workspaces
#5
by basyx - opened
- app/security/tenancy.py +56 -0
app/security/tenancy.py
CHANGED
|
@@ -100,6 +100,62 @@ class TenantService:
|
|
| 100 |
membership_role=membership.role,
|
| 101 |
)
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
async def bind_api_key(
|
| 104 |
self, *, api_key_id: str, workspace_id: str, user_id: str
|
| 105 |
) -> TenantPrincipal:
|
|
|
|
| 100 |
membership_role=membership.role,
|
| 101 |
)
|
| 102 |
|
| 103 |
+
async def resolve_studio_identity(
|
| 104 |
+
self, *, subject: str, email: str, display_name: str
|
| 105 |
+
) -> TenantPrincipal:
|
| 106 |
+
"""Resolve a verified Supabase subject to its own backend workspace."""
|
| 107 |
+
if not subject or len(subject) > 255:
|
| 108 |
+
raise UnauthorizedError
|
| 109 |
+
external_subject = f"supabase:{subject}"
|
| 110 |
+
async with self.database.session() as session:
|
| 111 |
+
user = await session.scalar(select(User).where(User.subject == external_subject))
|
| 112 |
+
if user is not None:
|
| 113 |
+
if user.status != "active":
|
| 114 |
+
raise ForbiddenError
|
| 115 |
+
membership = await session.scalar(
|
| 116 |
+
select(WorkspaceMembership).where(
|
| 117 |
+
WorkspaceMembership.user_id == user.id,
|
| 118 |
+
WorkspaceMembership.status == "active",
|
| 119 |
+
).order_by(WorkspaceMembership.created_at.asc())
|
| 120 |
+
)
|
| 121 |
+
if membership is None:
|
| 122 |
+
raise ForbiddenError
|
| 123 |
+
workspace = await session.get(Workspace, membership.workspace_id)
|
| 124 |
+
if workspace is None or workspace.status != "active":
|
| 125 |
+
raise ForbiddenError
|
| 126 |
+
return TenantPrincipal(workspace.id, user.id, membership.id, membership.role)
|
| 127 |
+
|
| 128 |
+
user_id, workspace_id, membership_id = str(uuid4()), str(uuid4()), str(uuid4())
|
| 129 |
+
safe_name = (display_name or email or "Studio user").strip()[:255] or "Studio user"
|
| 130 |
+
user = User(id=user_id, subject=external_subject, display_name=safe_name,
|
| 131 |
+
metadata_json={"identity_provider": "supabase"})
|
| 132 |
+
workspace = Workspace(id=workspace_id, slug=f"studio-{user_id.replace('-', '')}",
|
| 133 |
+
name=f"{safe_name}'s workspace"[:255],
|
| 134 |
+
metadata_json={"provisioned_from": "studio_auth"})
|
| 135 |
+
membership = WorkspaceMembership(id=membership_id, workspace_id=workspace_id,
|
| 136 |
+
user_id=user_id, role="admin", status="active")
|
| 137 |
+
session.add_all([user, workspace, membership])
|
| 138 |
+
try:
|
| 139 |
+
await session.commit()
|
| 140 |
+
except IntegrityError:
|
| 141 |
+
await session.rollback()
|
| 142 |
+
user = await session.scalar(select(User).where(User.subject == external_subject))
|
| 143 |
+
if user is None or user.status != "active":
|
| 144 |
+
raise
|
| 145 |
+
membership = await session.scalar(
|
| 146 |
+
select(WorkspaceMembership).where(
|
| 147 |
+
WorkspaceMembership.user_id == user.id,
|
| 148 |
+
WorkspaceMembership.status == "active",
|
| 149 |
+
).order_by(WorkspaceMembership.created_at.asc())
|
| 150 |
+
)
|
| 151 |
+
if membership is None:
|
| 152 |
+
raise ForbiddenError
|
| 153 |
+
workspace = await session.get(Workspace, membership.workspace_id)
|
| 154 |
+
if workspace is None or workspace.status != "active":
|
| 155 |
+
raise ForbiddenError
|
| 156 |
+
return TenantPrincipal(workspace.id, user.id, membership.id, membership.role)
|
| 157 |
+
return TenantPrincipal(workspace_id, user_id, membership_id, "admin")
|
| 158 |
+
|
| 159 |
async def bind_api_key(
|
| 160 |
self, *, api_key_id: str, workspace_id: str, user_id: str
|
| 161 |
) -> TenantPrincipal:
|