Refer permissions by UUID rather than scope.
This commit is contained in:
@@ -299,13 +299,13 @@ async function performPermissionDeletion(permissionScope) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deletePermission(p) {
|
function deletePermission(p) {
|
||||||
const userCount = permissionSummary.value[p.scope]?.userCount || 0
|
const userCount = permissionSummary.value[p.uuid]?.userCount || 0
|
||||||
|
|
||||||
// Count roles that have this permission
|
// Count roles that have this permission
|
||||||
let roleCount = 0
|
let roleCount = 0
|
||||||
for (const org of orgs.value) {
|
for (const org of orgs.value) {
|
||||||
for (const role of org.roles) {
|
for (const role of org.roles) {
|
||||||
if (role.permissions.includes(p.scope)) {
|
if (role.permissions.includes(p.uuid)) {
|
||||||
roleCount++
|
roleCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ const sortedRoles = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Get org's grantable permissions as full permission objects (with UUIDs)
|
||||||
|
const orgPermissions = computed(() => {
|
||||||
|
const uuidSet = new Set(props.selectedOrg.permissions || [])
|
||||||
|
return props.permissions.filter(p => uuidSet.has(p.uuid))
|
||||||
|
})
|
||||||
|
|
||||||
function permissionDisplayName(scope) {
|
function permissionDisplayName(scope) {
|
||||||
return props.permissions.find(p => p.scope === scope)?.display_name || scope
|
return props.permissions.find(p => p.scope === scope)?.display_name || scope
|
||||||
}
|
}
|
||||||
@@ -302,17 +308,17 @@ defineExpose({ focusFirstElement })
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid-head role-head add-role-head" title="Add role" @click="$emit('createRole', selectedOrg)" role="button" tabindex="0" @keydown.enter="$emit('createRole', selectedOrg)">➕</div>
|
<div class="grid-head role-head add-role-head" title="Add role" @click="$emit('createRole', selectedOrg)" role="button" tabindex="0" @keydown.enter="$emit('createRole', selectedOrg)">➕</div>
|
||||||
|
|
||||||
<template v-for="pid in selectedOrg.permissions" :key="pid">
|
<template v-for="p in orgPermissions" :key="p.uuid">
|
||||||
<div class="perm-name" :title="pid">{{ permissionDisplayName(pid) }}</div>
|
<div class="perm-name" :title="p.scope">{{ p.display_name }}</div>
|
||||||
<div
|
<div
|
||||||
v-for="r in sortedRoles"
|
v-for="r in sortedRoles"
|
||||||
:key="r.uuid + '-' + pid"
|
:key="r.uuid + '-' + p.uuid"
|
||||||
class="matrix-cell"
|
class="matrix-cell"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="r.permissions.includes(pid)"
|
:checked="r.permissions.includes(p.uuid)"
|
||||||
@change="e => toggleRolePermission(r, pid, e.target.checked)"
|
@change="e => toggleRolePermission(r, p.uuid, e.target.checked)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="matrix-cell add-role-cell" />
|
<div class="matrix-cell add-role-cell" />
|
||||||
|
|||||||
@@ -296,19 +296,19 @@ defineExpose({ focusFirstElement })
|
|||||||
<span>{{ o.display_name }}</span>
|
<span>{{ o.display_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-for="p in sortedPermissions" :key="p.scope">
|
<template v-for="p in sortedPermissions" :key="p.uuid">
|
||||||
<div class="perm-name" :title="p.scope">
|
<div class="perm-name" :title="p.scope">
|
||||||
<span class="display-text">{{ p.display_name }}</span>
|
<span class="display-text">{{ p.display_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="o in sortedOrgs"
|
v-for="o in sortedOrgs"
|
||||||
:key="o.uuid + '-' + p.scope"
|
:key="o.uuid + '-' + p.uuid"
|
||||||
class="matrix-cell"
|
class="matrix-cell"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="o.permissions.includes(p.scope)"
|
:checked="o.permissions.includes(p.uuid)"
|
||||||
@change="e => $emit('toggleOrgPermission', o, p.scope, e.target.checked)"
|
@change="e => $emit('toggleOrgPermission', o, p.uuid, e.target.checked)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -329,7 +329,7 @@ defineExpose({ focusFirstElement })
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="p in sortedPermissions" :key="p.scope">
|
<tr v-for="p in sortedPermissions" :key="p.uuid">
|
||||||
<td class="perm-name-cell">
|
<td class="perm-name-cell">
|
||||||
<div class="perm-title">
|
<div class="perm-title">
|
||||||
<span class="display-text">{{ p.display_name }}</span>
|
<span class="display-text">{{ p.display_name }}</span>
|
||||||
@@ -340,7 +340,7 @@ defineExpose({ focusFirstElement })
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="perm-domain">{{ p.domain || '—' }}</td>
|
<td class="perm-domain">{{ p.domain || '—' }}</td>
|
||||||
<td class="perm-members center">{{ permissionSummary[p.scope]?.userCount || 0 }}</td>
|
<td class="perm-members center">{{ permissionSummary[p.uuid]?.userCount || 0 }}</td>
|
||||||
<td class="perm-actions center">
|
<td class="perm-actions center">
|
||||||
<button @click="$emit('deletePermission', p)" class="icon-btn delete-icon" aria-label="Delete permission" title="Delete permission">❌</button>
|
<button @click="$emit('deletePermission', p)" class="icon-btn delete-icon" aria-label="Delete permission" title="Delete permission">❌</button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
+90
-51
@@ -170,14 +170,14 @@ def build_role(uuid: UUID) -> Role:
|
|||||||
uuid=uuid,
|
uuid=uuid,
|
||||||
org_uuid=r.org,
|
org_uuid=r.org,
|
||||||
display_name=r.display_name,
|
display_name=r.display_name,
|
||||||
permissions=list(r.permissions.keys()),
|
permissions=[str(pid) for pid in r.permissions.keys()],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_org(uuid: UUID, include_roles: bool = False) -> Org:
|
def build_org(uuid: UUID, include_roles: bool = False) -> Org:
|
||||||
o = _db._data.orgs[uuid]
|
o = _db._data.orgs[uuid]
|
||||||
perm_scopes = [p.scope for p in _db._data.permissions.values() if uuid in p.orgs]
|
perm_uuids = [str(pid) for pid, p in _db._data.permissions.items() if uuid in p.orgs]
|
||||||
org = Org(uuid=uuid, display_name=o.display_name, permissions=perm_scopes)
|
org = Org(uuid=uuid, display_name=o.display_name, permissions=perm_uuids)
|
||||||
if include_roles:
|
if include_roles:
|
||||||
org.roles = [
|
org.roles = [
|
||||||
build_role(rid) for rid, r in _db._data.roles.items() if r.org == uuid
|
build_role(rid) for rid, r in _db._data.roles.items() if r.org == uuid
|
||||||
@@ -461,24 +461,24 @@ def get_session_context(
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Effective permissions: role's permission scopes that the org can grant
|
# Effective permissions: role's permissions that the org can grant
|
||||||
# Also filter by domain if host is provided
|
# Also filter by domain if host is provided
|
||||||
org_scopes = set(org.permissions)
|
org_perm_uuids = set(org.permissions) # Set of permission UUID strings
|
||||||
normalized_host = normalize_host(host)
|
normalized_host = normalize_host(host)
|
||||||
host_without_port = normalized_host.rsplit(":", 1)[0] if normalized_host else None
|
host_without_port = normalized_host.rsplit(":", 1)[0] if normalized_host else None
|
||||||
|
|
||||||
effective_perms = []
|
effective_perms = []
|
||||||
for scope in role.permissions:
|
for perm_uuid_str in role.permissions:
|
||||||
if scope not in org_scopes:
|
if perm_uuid_str not in org_perm_uuids:
|
||||||
continue
|
continue
|
||||||
# Find permission by scope
|
perm_uuid = UUID(perm_uuid_str)
|
||||||
for pid, p in _db._data.permissions.items():
|
if perm_uuid not in _db._data.permissions:
|
||||||
if p.scope == scope:
|
continue
|
||||||
# Check domain restriction
|
p = _db._data.permissions[perm_uuid]
|
||||||
if p.domain is not None and p.domain != host_without_port:
|
# Check domain restriction
|
||||||
continue
|
if p.domain is not None and p.domain != host_without_port:
|
||||||
effective_perms.append(build_permission(pid))
|
continue
|
||||||
break
|
effective_perms.append(build_permission(perm_uuid))
|
||||||
|
|
||||||
return SessionContext(
|
return SessionContext(
|
||||||
session=session,
|
session=session,
|
||||||
@@ -527,7 +527,7 @@ def rename_permission(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Rename a permission's scope. The UUID remains the same.
|
"""Rename a permission's scope. The UUID remains the same.
|
||||||
|
|
||||||
Also updates all role references to use the new scope.
|
Since roles reference permissions by UUID, no role updates are needed.
|
||||||
Note: Scopes do not need to be unique (same scope with different domains is valid).
|
Note: Scopes do not need to be unique (same scope with different domains is valid).
|
||||||
"""
|
"""
|
||||||
# Find permission by old scope
|
# Find permission by old scope
|
||||||
@@ -545,21 +545,17 @@ def rename_permission(
|
|||||||
_db._data.permissions[key].display_name = display_name
|
_db._data.permissions[key].display_name = display_name
|
||||||
_db._data.permissions[key].domain = domain
|
_db._data.permissions[key].domain = domain
|
||||||
|
|
||||||
# Update role references if scope changed
|
|
||||||
if old_scope != new_scope:
|
|
||||||
for r in _db._data.roles.values():
|
|
||||||
if old_scope in r.permissions:
|
|
||||||
del r.permissions[old_scope]
|
|
||||||
r.permissions[new_scope] = True
|
|
||||||
|
|
||||||
|
|
||||||
def delete_permission(uuid: str | UUID, actor: str = "system") -> None:
|
def delete_permission(uuid: str | UUID, actor: str = "system") -> None:
|
||||||
"""Delete a permission."""
|
"""Delete a permission and remove it from all roles."""
|
||||||
if isinstance(uuid, str):
|
if isinstance(uuid, str):
|
||||||
uuid = UUID(uuid)
|
uuid = UUID(uuid)
|
||||||
if uuid not in _db._data.permissions:
|
if uuid not in _db._data.permissions:
|
||||||
raise ValueError(f"Permission {uuid} not found")
|
raise ValueError(f"Permission {uuid} not found")
|
||||||
with _db.transaction(actor):
|
with _db.transaction(actor):
|
||||||
|
# Remove this permission from all roles
|
||||||
|
for role in _db._data.roles.values():
|
||||||
|
role.permissions.pop(uuid, None)
|
||||||
del _db._data.permissions[uuid]
|
del _db._data.permissions[uuid]
|
||||||
|
|
||||||
|
|
||||||
@@ -574,19 +570,26 @@ def create_organization(org: Org, actor: str = "system") -> None:
|
|||||||
_db._data.orgs[org.uuid] = _OrgData(
|
_db._data.orgs[org.uuid] = _OrgData(
|
||||||
display_name=org.display_name, created_at=datetime.now(timezone.utc)
|
display_name=org.display_name, created_at=datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
# Grant listed permissions to this org
|
# Grant listed permissions to this org (org.permissions contains UUIDs now)
|
||||||
for scope in org.permissions:
|
for perm_uuid_str in org.permissions:
|
||||||
for pid, p in _db._data.permissions.items():
|
perm_uuid = UUID(perm_uuid_str) if isinstance(perm_uuid_str, str) else perm_uuid_str
|
||||||
if p.scope == scope:
|
if perm_uuid in _db._data.permissions:
|
||||||
p.orgs[org.uuid] = True
|
_db._data.permissions[perm_uuid].orgs[org.uuid] = True
|
||||||
# Create Administration role with org admin permission
|
# Create Administration role with org admin permission
|
||||||
import uuid7
|
import uuid7
|
||||||
|
|
||||||
admin_role_uuid = uuid7.create()
|
admin_role_uuid = uuid7.create()
|
||||||
|
# Find the auth:org:admin permission UUID
|
||||||
|
org_admin_perm_uuid = None
|
||||||
|
for pid, p in _db._data.permissions.items():
|
||||||
|
if p.scope == "auth:org:admin":
|
||||||
|
org_admin_perm_uuid = pid
|
||||||
|
break
|
||||||
|
role_permissions = {org_admin_perm_uuid: True} if org_admin_perm_uuid else {}
|
||||||
_db._data.roles[admin_role_uuid] = _RoleData(
|
_db._data.roles[admin_role_uuid] = _RoleData(
|
||||||
org=org.uuid,
|
org=org.uuid,
|
||||||
display_name="Administration",
|
display_name="Administration",
|
||||||
permissions={"auth:org:admin": True},
|
permissions=role_permissions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -624,35 +627,65 @@ def delete_organization(uuid: str | UUID, actor: str = "system") -> None:
|
|||||||
|
|
||||||
|
|
||||||
def add_permission_to_organization(
|
def add_permission_to_organization(
|
||||||
org_uuid: str | UUID, permission_scope: str, actor: str = "system"
|
org_uuid: str | UUID, permission_id: str | UUID, actor: str = "system"
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Grant a permission scope to an organization."""
|
"""Grant a permission to an organization by UUID."""
|
||||||
if isinstance(org_uuid, str):
|
if isinstance(org_uuid, str):
|
||||||
org_uuid = UUID(org_uuid)
|
org_uuid = UUID(org_uuid)
|
||||||
if org_uuid not in _db._data.orgs:
|
if org_uuid not in _db._data.orgs:
|
||||||
raise ValueError(f"Organization {org_uuid} not found")
|
raise ValueError(f"Organization {org_uuid} not found")
|
||||||
found = False
|
|
||||||
|
# Convert permission_id to UUID
|
||||||
|
if isinstance(permission_id, str):
|
||||||
|
try:
|
||||||
|
permission_uuid = UUID(permission_id)
|
||||||
|
except ValueError:
|
||||||
|
# It's a scope - look up the UUID (backwards compat)
|
||||||
|
for pid, p in _db._data.permissions.items():
|
||||||
|
if p.scope == permission_id:
|
||||||
|
permission_uuid = pid
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Permission {permission_id} not found")
|
||||||
|
else:
|
||||||
|
permission_uuid = permission_id
|
||||||
|
|
||||||
|
if permission_uuid not in _db._data.permissions:
|
||||||
|
raise ValueError(f"Permission {permission_uuid} not found")
|
||||||
|
|
||||||
with _db.transaction(actor):
|
with _db.transaction(actor):
|
||||||
for p in _db._data.permissions.values():
|
_db._data.permissions[permission_uuid].orgs[org_uuid] = True
|
||||||
if p.scope == permission_scope:
|
|
||||||
p.orgs[org_uuid] = True
|
|
||||||
found = True
|
|
||||||
if not found:
|
|
||||||
raise ValueError(f"Permission scope {permission_scope} not found")
|
|
||||||
|
|
||||||
|
|
||||||
def remove_permission_from_organization(
|
def remove_permission_from_organization(
|
||||||
org_uuid: str | UUID, permission_scope: str, actor: str = "system"
|
org_uuid: str | UUID, permission_id: str | UUID, actor: str = "system"
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Remove a permission scope from an organization."""
|
"""Remove a permission from an organization by UUID."""
|
||||||
if isinstance(org_uuid, str):
|
if isinstance(org_uuid, str):
|
||||||
org_uuid = UUID(org_uuid)
|
org_uuid = UUID(org_uuid)
|
||||||
if org_uuid not in _db._data.orgs:
|
if org_uuid not in _db._data.orgs:
|
||||||
raise ValueError(f"Organization {org_uuid} not found")
|
raise ValueError(f"Organization {org_uuid} not found")
|
||||||
|
|
||||||
|
# Convert permission_id to UUID
|
||||||
|
if isinstance(permission_id, str):
|
||||||
|
try:
|
||||||
|
permission_uuid = UUID(permission_id)
|
||||||
|
except ValueError:
|
||||||
|
# It's a scope - look up the UUID (backwards compat)
|
||||||
|
for pid, p in _db._data.permissions.items():
|
||||||
|
if p.scope == permission_id:
|
||||||
|
permission_uuid = pid
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return # Permission not found, silently return
|
||||||
|
else:
|
||||||
|
permission_uuid = permission_id
|
||||||
|
|
||||||
|
if permission_uuid not in _db._data.permissions:
|
||||||
|
return # Permission not found, silently return
|
||||||
|
|
||||||
with _db.transaction(actor):
|
with _db.transaction(actor):
|
||||||
for p in _db._data.permissions.values():
|
_db._data.permissions[permission_uuid].orgs.pop(org_uuid, None)
|
||||||
if p.scope == permission_scope:
|
|
||||||
p.orgs.pop(org_uuid, None)
|
|
||||||
|
|
||||||
|
|
||||||
def create_role(role: Role, actor: str = "system") -> None:
|
def create_role(role: Role, actor: str = "system") -> None:
|
||||||
@@ -665,7 +698,7 @@ def create_role(role: Role, actor: str = "system") -> None:
|
|||||||
_db._data.roles[role.uuid] = _RoleData(
|
_db._data.roles[role.uuid] = _RoleData(
|
||||||
org=role.org_uuid,
|
org=role.org_uuid,
|
||||||
display_name=role.display_name,
|
display_name=role.display_name,
|
||||||
permissions={scope: True for scope in role.permissions},
|
permissions={UUID(pid): True for pid in role.permissions},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -682,27 +715,33 @@ def update_role_name(
|
|||||||
|
|
||||||
|
|
||||||
def add_permission_to_role(
|
def add_permission_to_role(
|
||||||
role_uuid: str | UUID, permission_scope: str, actor: str = "system"
|
role_uuid: str | UUID, permission_uuid: str | UUID, actor: str = "system"
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add permission scope to role."""
|
"""Add permission to role by UUID."""
|
||||||
if isinstance(role_uuid, str):
|
if isinstance(role_uuid, str):
|
||||||
role_uuid = UUID(role_uuid)
|
role_uuid = UUID(role_uuid)
|
||||||
|
if isinstance(permission_uuid, str):
|
||||||
|
permission_uuid = UUID(permission_uuid)
|
||||||
if role_uuid not in _db._data.roles:
|
if role_uuid not in _db._data.roles:
|
||||||
raise ValueError(f"Role {role_uuid} not found")
|
raise ValueError(f"Role {role_uuid} not found")
|
||||||
|
if permission_uuid not in _db._data.permissions:
|
||||||
|
raise ValueError(f"Permission {permission_uuid} not found")
|
||||||
with _db.transaction(actor):
|
with _db.transaction(actor):
|
||||||
_db._data.roles[role_uuid].permissions[permission_scope] = True
|
_db._data.roles[role_uuid].permissions[permission_uuid] = True
|
||||||
|
|
||||||
|
|
||||||
def remove_permission_from_role(
|
def remove_permission_from_role(
|
||||||
role_uuid: str | UUID, permission_scope: str, actor: str = "system"
|
role_uuid: str | UUID, permission_uuid: str | UUID, actor: str = "system"
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Remove permission scope from role."""
|
"""Remove permission from role by UUID."""
|
||||||
if isinstance(role_uuid, str):
|
if isinstance(role_uuid, str):
|
||||||
role_uuid = UUID(role_uuid)
|
role_uuid = UUID(role_uuid)
|
||||||
|
if isinstance(permission_uuid, str):
|
||||||
|
permission_uuid = UUID(permission_uuid)
|
||||||
if role_uuid not in _db._data.roles:
|
if role_uuid not in _db._data.roles:
|
||||||
raise ValueError(f"Role {role_uuid} not found")
|
raise ValueError(f"Role {role_uuid} not found")
|
||||||
with _db.transaction(actor):
|
with _db.transaction(actor):
|
||||||
_db._data.roles[role_uuid].permissions.pop(permission_scope, None)
|
_db._data.roles[role_uuid].permissions.pop(permission_uuid, None)
|
||||||
|
|
||||||
|
|
||||||
def delete_role(uuid: str | UUID, actor: str = "system") -> None:
|
def delete_role(uuid: str | UUID, actor: str = "system") -> None:
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ class Role(msgspec.Struct):
|
|||||||
uuid: UUID
|
uuid: UUID
|
||||||
org_uuid: UUID
|
org_uuid: UUID
|
||||||
display_name: str
|
display_name: str
|
||||||
permissions: list[str] = [] # permission IDs this role grants
|
permissions: list[str] = [] # permission UUIDs this role grants
|
||||||
|
|
||||||
|
|
||||||
class Org(msgspec.Struct):
|
class Org(msgspec.Struct):
|
||||||
uuid: UUID
|
uuid: UUID
|
||||||
display_name: str
|
display_name: str
|
||||||
permissions: list[str] = [] # permission IDs this org can grant
|
permissions: list[str] = [] # permission UUIDs this org can grant
|
||||||
roles: list[Role] = [] # roles belonging to this org
|
roles: list[Role] = [] # roles belonging to this org
|
||||||
|
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class _OrgData(msgspec.Struct):
|
|||||||
class _RoleData(msgspec.Struct):
|
class _RoleData(msgspec.Struct):
|
||||||
org: UUID
|
org: UUID
|
||||||
display_name: str
|
display_name: str
|
||||||
permissions: dict[str, bool] # permission_id -> True
|
permissions: dict[UUID, bool] # permission_uuid -> True
|
||||||
|
|
||||||
|
|
||||||
class _UserData(msgspec.Struct):
|
class _UserData(msgspec.Struct):
|
||||||
|
|||||||
+22
-14
@@ -319,11 +319,11 @@ async def admin_update_role_name(
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/orgs/{org_uuid}/roles/{role_uuid}/permissions/{permission_id}")
|
@app.post("/orgs/{org_uuid}/roles/{role_uuid}/permissions/{permission_uuid}")
|
||||||
async def admin_add_role_permission(
|
async def admin_add_role_permission(
|
||||||
org_uuid: UUID,
|
org_uuid: UUID,
|
||||||
role_uuid: UUID,
|
role_uuid: UUID,
|
||||||
permission_id: str,
|
permission_uuid: UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
auth=AUTH_COOKIE,
|
auth=AUTH_COOKIE,
|
||||||
):
|
):
|
||||||
@@ -344,20 +344,22 @@ async def admin_add_role_permission(
|
|||||||
raise HTTPException(status_code=404, detail="Role not found in organization")
|
raise HTTPException(status_code=404, detail="Role not found in organization")
|
||||||
|
|
||||||
# Verify permission exists and org can grant it
|
# Verify permission exists and org can grant it
|
||||||
db.get_permission(permission_id)
|
perm = db.get_permission(permission_uuid)
|
||||||
|
if not perm:
|
||||||
|
raise HTTPException(status_code=404, detail="Permission not found")
|
||||||
org = db.get_organization(str(org_uuid))
|
org = db.get_organization(str(org_uuid))
|
||||||
if permission_id not in org.permissions:
|
if str(permission_uuid) not in org.permissions:
|
||||||
raise ValueError("Permission not grantable by organization")
|
raise ValueError("Permission not grantable by organization")
|
||||||
|
|
||||||
db.add_permission_to_role(role_uuid, permission_id, actor=str(ctx.user.uuid))
|
db.add_permission_to_role(role_uuid, permission_uuid, actor=str(ctx.user.uuid))
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/orgs/{org_uuid}/roles/{role_uuid}/permissions/{permission_id}")
|
@app.delete("/orgs/{org_uuid}/roles/{role_uuid}/permissions/{permission_uuid}")
|
||||||
async def admin_remove_role_permission(
|
async def admin_remove_role_permission(
|
||||||
org_uuid: UUID,
|
org_uuid: UUID,
|
||||||
role_uuid: UUID,
|
role_uuid: UUID,
|
||||||
permission_id: str,
|
permission_uuid: UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
auth=AUTH_COOKIE,
|
auth=AUTH_COOKIE,
|
||||||
):
|
):
|
||||||
@@ -378,17 +380,23 @@ async def admin_remove_role_permission(
|
|||||||
raise HTTPException(status_code=404, detail="Role not found in organization")
|
raise HTTPException(status_code=404, detail="Role not found in organization")
|
||||||
|
|
||||||
# Sanity check: prevent admin from removing their own access
|
# Sanity check: prevent admin from removing their own access
|
||||||
|
# Find auth:admin and auth:org:admin permission UUIDs
|
||||||
|
perm_uuid_str = str(permission_uuid)
|
||||||
|
perm = db.get_permission(permission_uuid)
|
||||||
if ctx.org.uuid == org_uuid and ctx.role.uuid == role_uuid:
|
if ctx.org.uuid == org_uuid and ctx.role.uuid == role_uuid:
|
||||||
if permission_id in ["auth:admin", "auth:org:admin"]:
|
if perm and perm.scope in ["auth:admin", "auth:org:admin"]:
|
||||||
# Check if removing this permission would leave no admin access
|
# Check if removing this permission would leave no admin access
|
||||||
remaining_perms = set(role.permissions) - {permission_id}
|
remaining_perms = set(role.permissions) - {perm_uuid_str}
|
||||||
if (
|
has_admin = False
|
||||||
"auth:admin" not in remaining_perms
|
for rp_uuid in remaining_perms:
|
||||||
and "auth:org:admin" not in remaining_perms
|
rp = db.get_permission(rp_uuid)
|
||||||
):
|
if rp and rp.scope in ["auth:admin", "auth:org:admin"]:
|
||||||
|
has_admin = True
|
||||||
|
break
|
||||||
|
if not has_admin:
|
||||||
raise ValueError("Cannot remove your own admin permissions")
|
raise ValueError("Cannot remove your own admin permissions")
|
||||||
|
|
||||||
db.remove_permission_from_role(role_uuid, permission_id, actor=str(ctx.user.uuid))
|
db.remove_permission_from_role(role_uuid, permission_uuid, actor=str(ctx.user.uuid))
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+15
-17
@@ -96,19 +96,19 @@ async def migrate_from_sql(
|
|||||||
orgs={},
|
orgs={},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mapping from old permission ID to new scope
|
# Mapping from old permission ID to new permission UUID
|
||||||
perm_id_to_scope: dict[str, str] = {}
|
perm_id_to_uuid: dict[str, UUID] = {}
|
||||||
|
|
||||||
for perm in permissions:
|
for perm in permissions:
|
||||||
# Skip old org-specific admin permissions (auth:org:{uuid}) - they map to auth:org:admin
|
# Skip old org-specific admin permissions (auth:org:{uuid}) - they map to auth:org:admin
|
||||||
match = old_org_admin_pattern.match(perm.id)
|
match = old_org_admin_pattern.match(perm.id)
|
||||||
if match:
|
if match:
|
||||||
perm_id_to_scope[perm.id] = "auth:org:admin"
|
perm_id_to_uuid[perm.id] = org_admin_perm_uuid
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip if this is already auth:org:admin - we created one above
|
# Skip if this is already auth:org:admin - we created one above
|
||||||
if perm.id == "auth:org:admin":
|
if perm.id == "auth:org:admin":
|
||||||
perm_id_to_scope[perm.id] = "auth:org:admin"
|
perm_id_to_uuid[perm.id] = org_admin_perm_uuid
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Regular permission - create with UUID key
|
# Regular permission - create with UUID key
|
||||||
@@ -118,7 +118,7 @@ async def migrate_from_sql(
|
|||||||
display_name=perm.display_name,
|
display_name=perm.display_name,
|
||||||
orgs={},
|
orgs={},
|
||||||
)
|
)
|
||||||
perm_id_to_scope[perm.id] = perm.id # Scope same as old ID
|
perm_id_to_uuid[perm.id] = perm_uuid
|
||||||
print(
|
print(
|
||||||
f" Migrated {len(permissions)} permissions (with {len(org_admin_uuids)} org-specific admins consolidated to auth:org:admin)"
|
f" Migrated {len(permissions)} permissions (with {len(org_admin_uuids)} org-specific admins consolidated to auth:org:admin)"
|
||||||
)
|
)
|
||||||
@@ -130,28 +130,26 @@ async def migrate_from_sql(
|
|||||||
json_db._data.orgs[org_key] = _OrgData(
|
json_db._data.orgs[org_key] = _OrgData(
|
||||||
display_name=org.display_name,
|
display_name=org.display_name,
|
||||||
)
|
)
|
||||||
# Update permissions to allow this org to grant them (by scope)
|
# Update permissions to allow this org to grant them (by UUID)
|
||||||
for old_perm_id in org.permissions:
|
for old_perm_id in org.permissions:
|
||||||
new_scope = perm_id_to_scope.get(old_perm_id, old_perm_id)
|
perm_uuid = perm_id_to_uuid.get(old_perm_id)
|
||||||
# Find permission with this scope and add org
|
if perm_uuid and perm_uuid in json_db._data.permissions:
|
||||||
for pid, p in json_db._data.permissions.items():
|
json_db._data.permissions[perm_uuid].orgs[org_key] = True
|
||||||
if p.scope == new_scope:
|
|
||||||
p.orgs[org_key] = True
|
|
||||||
break
|
|
||||||
# Ensure every org can grant auth:org:admin
|
# Ensure every org can grant auth:org:admin
|
||||||
json_db._data.permissions[org_admin_perm_uuid].orgs[org_key] = True
|
json_db._data.permissions[org_admin_perm_uuid].orgs[org_key] = True
|
||||||
print(f" Migrated {len(orgs)} organizations")
|
print(f" Migrated {len(orgs)} organizations")
|
||||||
|
|
||||||
# Migrate roles - convert old permission IDs to scopes
|
# Migrate roles - convert old permission IDs to UUIDs
|
||||||
role_count = 0
|
role_count = 0
|
||||||
for org in orgs:
|
for org in orgs:
|
||||||
for role in org.roles:
|
for role in org.roles:
|
||||||
role_key: UUID = role.uuid
|
role_key: UUID = role.uuid
|
||||||
# Convert old permission IDs to scopes
|
# Convert old permission IDs to UUIDs
|
||||||
new_permissions = {}
|
new_permissions: dict[UUID, bool] = {}
|
||||||
for old_perm_id in role.permissions or []:
|
for old_perm_id in role.permissions or []:
|
||||||
new_scope = perm_id_to_scope.get(old_perm_id, old_perm_id)
|
perm_uuid = perm_id_to_uuid.get(old_perm_id)
|
||||||
new_permissions[new_scope] = True
|
if perm_uuid:
|
||||||
|
new_permissions[perm_uuid] = True
|
||||||
json_db._data.roles[role_key] = _RoleData(
|
json_db._data.roles[role_key] = _RoleData(
|
||||||
org=role.org_uuid,
|
org=role.org_uuid,
|
||||||
display_name=role.display_name,
|
display_name=role.display_name,
|
||||||
|
|||||||
Reference in New Issue
Block a user