Major refactoring of admin API (permissions, paths)
This commit is contained in:
parent
bfc777fb56
commit
c9f9b28bf4
@ -147,7 +147,7 @@ async function loadOrgs() {
|
|||||||
if (data.detail) throw new Error(data.detail)
|
if (data.detail) throw new Error(data.detail)
|
||||||
// Restructure to attach users to roles instead of flat user list at org level
|
// Restructure to attach users to roles instead of flat user list at org level
|
||||||
orgs.value = data.map(o => {
|
orgs.value = data.map(o => {
|
||||||
const roles = o.roles.map(r => ({ ...r, users: [] }))
|
const roles = o.roles.map(r => ({ ...r, org_uuid: o.uuid, users: [] }))
|
||||||
const roleMap = Object.fromEntries(roles.map(r => [r.display_name, r]))
|
const roleMap = Object.fromEntries(roles.map(r => [r.display_name, r]))
|
||||||
for (const u of o.users || []) {
|
for (const u of o.users || []) {
|
||||||
if (roleMap[u.role]) roleMap[u.role].users.push(u)
|
if (roleMap[u.role]) roleMap[u.role].users.push(u)
|
||||||
@ -250,7 +250,7 @@ function updateRole(role) { openDialog('role-update', { role }) }
|
|||||||
|
|
||||||
function deleteRole(role) {
|
function deleteRole(role) {
|
||||||
openDialog('confirm', { message: `Delete role ${role.display_name}?`, action: async () => {
|
openDialog('confirm', { message: `Delete role ${role.display_name}?`, action: async () => {
|
||||||
const res = await fetch(`/auth/admin/roles/${role.uuid}`, { method: 'DELETE' })
|
const res = await fetch(`/auth/admin/orgs/${role.org_uuid}/roles/${role.uuid}`, { method: 'DELETE' })
|
||||||
const data = await res.json(); if (data.detail) throw new Error(data.detail)
|
const data = await res.json(); if (data.detail) throw new Error(data.detail)
|
||||||
await loadOrgs()
|
await loadOrgs()
|
||||||
} })
|
} })
|
||||||
@ -321,7 +321,7 @@ const pageHeading = computed(() => {
|
|||||||
watch(selectedUser, async (u) => {
|
watch(selectedUser, async (u) => {
|
||||||
if (!u) { userDetail.value = null; return }
|
if (!u) { userDetail.value = null; return }
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/auth/admin/users/${u.uuid}`)
|
const res = await fetch(`/auth/admin/orgs/${u.org_uuid}/users/${u.uuid}`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.detail) throw new Error(data.detail)
|
if (data.detail) throw new Error(data.detail)
|
||||||
userDetail.value = data
|
userDetail.value = data
|
||||||
@ -359,7 +359,7 @@ async function toggleRolePermission(role, permId, checked) {
|
|||||||
const prev = [...role.permissions]
|
const prev = [...role.permissions]
|
||||||
role.permissions = next
|
role.permissions = next
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/auth/admin/roles/${role.uuid}`, {
|
const res = await fetch(`/auth/admin/orgs/${role.org_uuid}/roles/${role.uuid}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({ display_name: role.display_name, permissions: next })
|
body: JSON.stringify({ display_name: role.display_name, permissions: next })
|
||||||
@ -379,7 +379,7 @@ async function onUserNameSaved() {
|
|||||||
await loadOrgs()
|
await loadOrgs()
|
||||||
if (selectedUser.value) {
|
if (selectedUser.value) {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/auth/admin/users/${selectedUser.value.uuid}`)
|
const r = await fetch(`/auth/admin/orgs/${selectedUser.value.org_uuid}/users/${selectedUser.value.uuid}`)
|
||||||
const jd = await r.json()
|
const jd = await r.json()
|
||||||
if (!r.ok || jd.detail) throw new Error(jd.detail || 'Reload failed')
|
if (!r.ok || jd.detail) throw new Error(jd.detail || 'Reload failed')
|
||||||
userDetail.value = jd
|
userDetail.value = jd
|
||||||
@ -409,7 +409,7 @@ async function submitDialog() {
|
|||||||
const { role } = dialog.value.data; const name = dialog.value.data.name?.trim(); if (!name) throw new Error('Name required')
|
const { role } = dialog.value.data; const name = dialog.value.data.name?.trim(); if (!name) throw new Error('Name required')
|
||||||
const permsCsv = dialog.value.data.perms || ''
|
const permsCsv = dialog.value.data.perms || ''
|
||||||
const perms = permsCsv.split(',').map(s=>s.trim()).filter(Boolean)
|
const perms = permsCsv.split(',').map(s=>s.trim()).filter(Boolean)
|
||||||
const res = await fetch(`/auth/admin/roles/${role.uuid}`, { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ display_name: name, permissions: perms }) })
|
const res = await fetch(`/auth/admin/orgs/${role.org_uuid}/roles/${role.uuid}`, { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ display_name: name, permissions: perms }) })
|
||||||
const d = await res.json(); if (d.detail) throw new Error(d.detail); await loadOrgs()
|
const d = await res.json(); if (d.detail) throw new Error(d.detail); await loadOrgs()
|
||||||
} else if (t === 'user-create') {
|
} else if (t === 'user-create') {
|
||||||
const { org, role } = dialog.value.data; const name = dialog.value.data.name?.trim(); if (!name) throw new Error('Name required')
|
const { org, role } = dialog.value.data; const name = dialog.value.data.name?.trim(); if (!name) throw new Error('Name required')
|
||||||
@ -496,7 +496,7 @@ async function submitDialog() {
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:org-display-name="userDetail.org.display_name"
|
:org-display-name="userDetail.org.display_name"
|
||||||
:role-name="userDetail.role"
|
:role-name="userDetail.role"
|
||||||
:update-endpoint="`/auth/admin/users/${selectedUser.uuid}/display-name`"
|
:update-endpoint="`/auth/admin/orgs/${selectedUser.org_uuid}/users/${selectedUser.uuid}/display-name`"
|
||||||
@saved="onUserNameSaved"
|
@saved="onUserNameSaved"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="userDetail?.error" class="error small">{{ userDetail.error }}</div>
|
<div v-else-if="userDetail?.error" class="error small">{{ userDetail.error }}</div>
|
||||||
@ -512,7 +512,7 @@ async function submitDialog() {
|
|||||||
<p class="matrix-hint muted">Use the token dialog to register a new credential for the member.</p>
|
<p class="matrix-hint muted">Use the token dialog to register a new credential for the member.</p>
|
||||||
<RegistrationLinkModal
|
<RegistrationLinkModal
|
||||||
v-if="showRegModal"
|
v-if="showRegModal"
|
||||||
:endpoint="`/auth/admin/users/${selectedUser.uuid}/create-link`"
|
:endpoint="`/auth/admin/orgs/${selectedUser.org_uuid}/users/${selectedUser.uuid}/create-link`"
|
||||||
:auto-copy="false"
|
:auto-copy="false"
|
||||||
@close="showRegModal = false"
|
@close="showRegModal = false"
|
||||||
@copied="onLinkCopied"
|
@copied="onLinkCopied"
|
||||||
|
@ -8,6 +8,7 @@ from ..authsession import expires
|
|||||||
from ..globals import db
|
from ..globals import db
|
||||||
from ..globals import passkey as global_passkey
|
from ..globals import passkey as global_passkey
|
||||||
from ..util import frontend, passphrase, permutil, querysafe, tokens
|
from ..util import frontend, passphrase, permutil, querysafe, tokens
|
||||||
|
from . import authz
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
@ -25,10 +26,11 @@ async def general_exception_handler(_request, exc: Exception):
|
|||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def admin_frontend(auth=Cookie(None)):
|
async def admin_frontend(auth=Cookie(None)):
|
||||||
ctx = await permutil.session_context(auth)
|
try:
|
||||||
if permutil.has_any(ctx, ["auth:admin", "auth:org:*"]):
|
await authz.verify(auth, ["auth:admin", "auth:org:*"], match=permutil.has_any)
|
||||||
return FileResponse(frontend.file("admin/index.html"))
|
return FileResponse(frontend.file("admin/index.html"))
|
||||||
return FileResponse(frontend.file("index.html"), status_code=401 if ctx else 403)
|
except HTTPException as e:
|
||||||
|
return FileResponse(frontend.file("index.html"), status_code=e.status_code)
|
||||||
|
|
||||||
|
|
||||||
# -------------------- Organizations --------------------
|
# -------------------- Organizations --------------------
|
||||||
@ -36,12 +38,10 @@ async def admin_frontend(auth=Cookie(None)):
|
|||||||
|
|
||||||
@app.get("/orgs")
|
@app.get("/orgs")
|
||||||
async def admin_list_orgs(auth=Cookie(None)):
|
async def admin_list_orgs(auth=Cookie(None)):
|
||||||
ctx = await permutil.session_context(auth)
|
ctx = await authz.verify(auth, ["auth:admin", "auth:org:*"], match=permutil.has_any)
|
||||||
if not permutil.has_any(ctx, ["auth:admin", "auth:org:*"]):
|
|
||||||
raise ValueError("Insufficient permissions")
|
|
||||||
orgs = await db.instance.list_organizations()
|
orgs = await db.instance.list_organizations()
|
||||||
if not permutil.has_any(ctx, ["auth:admin"]): # limit org admin to their own org
|
if "auth:admin" not in ctx.role.permissions:
|
||||||
orgs = [o for o in orgs if o.uuid == ctx.org.uuid]
|
orgs = [o for o in orgs if f"auth:org:{o.uuid}" in ctx.role.permissions]
|
||||||
|
|
||||||
def role_to_dict(r):
|
def role_to_dict(r):
|
||||||
return {
|
return {
|
||||||
@ -75,9 +75,7 @@ async def admin_list_orgs(auth=Cookie(None)):
|
|||||||
|
|
||||||
@app.post("/orgs")
|
@app.post("/orgs")
|
||||||
async def admin_create_org(payload: dict = Body(...), auth=Cookie(None)):
|
async def admin_create_org(payload: dict = Body(...), auth=Cookie(None)):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(auth, ["auth:admin"])
|
||||||
if not permutil.has_any(ctx, ["auth:admin"]):
|
|
||||||
raise ValueError("Global admin required")
|
|
||||||
from ..db import Org as OrgDC # local import to avoid cycles
|
from ..db import Org as OrgDC # local import to avoid cycles
|
||||||
|
|
||||||
org_uuid = uuid4()
|
org_uuid = uuid4()
|
||||||
@ -92,9 +90,9 @@ async def admin_create_org(payload: dict = Body(...), auth=Cookie(None)):
|
|||||||
async def admin_update_org(
|
async def admin_update_org(
|
||||||
org_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
org_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
||||||
):
|
):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(
|
||||||
if not permutil.has_any(ctx, ["auth:admin"]):
|
auth, ["auth:admin", f"auth:org:{org_uuid}"], match=permutil.has_any
|
||||||
raise ValueError("Global admin required")
|
)
|
||||||
from ..db import Org as OrgDC # local import to avoid cycles
|
from ..db import Org as OrgDC # local import to avoid cycles
|
||||||
|
|
||||||
current = await db.instance.get_organization(str(org_uuid))
|
current = await db.instance.get_organization(str(org_uuid))
|
||||||
@ -107,14 +105,10 @@ async def admin_update_org(
|
|||||||
|
|
||||||
@app.delete("/orgs/{org_uuid}")
|
@app.delete("/orgs/{org_uuid}")
|
||||||
async def admin_delete_org(org_uuid: UUID, auth=Cookie(None)):
|
async def admin_delete_org(org_uuid: UUID, auth=Cookie(None)):
|
||||||
ctx = await permutil.session_context(auth)
|
ctx = await authz.verify(
|
||||||
if not permutil.has_any(ctx, ["auth:admin"]):
|
auth, ["auth:admin", f"auth:org:{org_uuid}"], match=permutil.has_any
|
||||||
raise ValueError("Global admin required")
|
)
|
||||||
try:
|
if ctx.org.uuid == org_uuid:
|
||||||
acting_org_uuid = ctx.org.uuid if ctx.org else None
|
|
||||||
except Exception: # pragma: no cover - defensive
|
|
||||||
acting_org_uuid = None
|
|
||||||
if acting_org_uuid and acting_org_uuid == org_uuid:
|
|
||||||
raise ValueError("Cannot delete the organization you belong to")
|
raise ValueError("Cannot delete the organization you belong to")
|
||||||
await db.instance.delete_organization(org_uuid)
|
await db.instance.delete_organization(org_uuid)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
@ -124,15 +118,7 @@ async def admin_delete_org(org_uuid: UUID, auth=Cookie(None)):
|
|||||||
async def admin_add_org_permission(
|
async def admin_add_org_permission(
|
||||||
org_uuid: UUID, permission_id: str, auth=Cookie(None)
|
org_uuid: UUID, permission_id: str, auth=Cookie(None)
|
||||||
):
|
):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(auth, ["auth:admin"])
|
||||||
if not (
|
|
||||||
permutil.has_any(ctx, ["auth:admin"])
|
|
||||||
or (
|
|
||||||
permutil.has_any(ctx, [f"auth:org:{org_uuid}"]) and ctx.org.uuid == org_uuid
|
|
||||||
)
|
|
||||||
):
|
|
||||||
raise ValueError("Insufficient permissions")
|
|
||||||
querysafe.assert_safe(permission_id, field="permission_id")
|
|
||||||
await db.instance.add_permission_to_organization(str(org_uuid), permission_id)
|
await db.instance.add_permission_to_organization(str(org_uuid), permission_id)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
@ -141,16 +127,7 @@ async def admin_add_org_permission(
|
|||||||
async def admin_remove_org_permission(
|
async def admin_remove_org_permission(
|
||||||
org_uuid: UUID, permission_id: str, auth=Cookie(None)
|
org_uuid: UUID, permission_id: str, auth=Cookie(None)
|
||||||
):
|
):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(auth, ["auth:admin"])
|
||||||
if not ctx or not (
|
|
||||||
permutil.has_any(ctx, ["auth:admin"])
|
|
||||||
or (
|
|
||||||
permutil.has_any(ctx, [f"auth:org:{org_uuid}"])
|
|
||||||
and getattr(ctx.org, "uuid", None) == org_uuid
|
|
||||||
)
|
|
||||||
):
|
|
||||||
raise ValueError("Insufficient permissions")
|
|
||||||
querysafe.assert_safe(permission_id, field="permission_id")
|
|
||||||
await db.instance.remove_permission_from_organization(str(org_uuid), permission_id)
|
await db.instance.remove_permission_from_organization(str(org_uuid), permission_id)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
@ -162,15 +139,7 @@ async def admin_remove_org_permission(
|
|||||||
async def admin_create_role(
|
async def admin_create_role(
|
||||||
org_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
org_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
||||||
):
|
):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(auth, ["auth:admin", f"auth:org:{org_uuid}"])
|
||||||
if not ctx or not (
|
|
||||||
permutil.has_any(ctx, ["auth:admin"])
|
|
||||||
or (
|
|
||||||
permutil.has_any(ctx, [f"auth:org:{org_uuid}"])
|
|
||||||
and getattr(ctx.org, "uuid", None) == org_uuid
|
|
||||||
)
|
|
||||||
):
|
|
||||||
raise ValueError("Insufficient permissions")
|
|
||||||
from ..db import Role as RoleDC
|
from ..db import Role as RoleDC
|
||||||
|
|
||||||
role_uuid = uuid4()
|
role_uuid = uuid4()
|
||||||
@ -192,25 +161,22 @@ async def admin_create_role(
|
|||||||
return {"uuid": str(role_uuid)}
|
return {"uuid": str(role_uuid)}
|
||||||
|
|
||||||
|
|
||||||
@app.put("/roles/{role_uuid}")
|
@app.put("/orgs/{org_uuid}/roles/{role_uuid}")
|
||||||
async def admin_update_role(
|
async def admin_update_role(
|
||||||
role_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
org_uuid: UUID, role_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
||||||
):
|
):
|
||||||
ctx = await permutil.session_context(auth)
|
# Verify caller is global admin or admin of provided org
|
||||||
role = await db.instance.get_role(role_uuid)
|
await authz.verify(
|
||||||
if not ctx or not (
|
auth, ["auth:admin", f"auth:org:{org_uuid}"], match=permutil.has_any
|
||||||
permutil.has_any(ctx, ["auth:admin"])
|
|
||||||
or (
|
|
||||||
permutil.has_any(ctx, [f"auth:org:{role.org_uuid}"])
|
|
||||||
and getattr(ctx.org, "uuid", None) == role.org_uuid
|
|
||||||
)
|
)
|
||||||
):
|
role = await db.instance.get_role(role_uuid)
|
||||||
raise ValueError("Insufficient permissions")
|
if role.org_uuid != org_uuid:
|
||||||
|
raise HTTPException(status_code=404, detail="Role not found in organization")
|
||||||
from ..db import Role as RoleDC
|
from ..db import Role as RoleDC
|
||||||
|
|
||||||
display_name = payload.get("display_name") or role.display_name
|
display_name = payload.get("display_name") or role.display_name
|
||||||
permissions = payload.get("permissions") or role.permissions
|
permissions = payload.get("permissions") or role.permissions
|
||||||
org = await db.instance.get_organization(str(role.org_uuid))
|
org = await db.instance.get_organization(str(org_uuid))
|
||||||
grantable = set(org.permissions or [])
|
grantable = set(org.permissions or [])
|
||||||
for pid in permissions:
|
for pid in permissions:
|
||||||
await db.instance.get_permission(pid)
|
await db.instance.get_permission(pid)
|
||||||
@ -218,7 +184,7 @@ async def admin_update_role(
|
|||||||
raise ValueError(f"Permission not grantable by org: {pid}")
|
raise ValueError(f"Permission not grantable by org: {pid}")
|
||||||
updated = RoleDC(
|
updated = RoleDC(
|
||||||
uuid=role_uuid,
|
uuid=role_uuid,
|
||||||
org_uuid=role.org_uuid,
|
org_uuid=org_uuid,
|
||||||
display_name=display_name,
|
display_name=display_name,
|
||||||
permissions=permissions,
|
permissions=permissions,
|
||||||
)
|
)
|
||||||
@ -226,18 +192,14 @@ async def admin_update_role(
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/roles/{role_uuid}")
|
@app.delete("/orgs/{org_uuid}/roles/{role_uuid}")
|
||||||
async def admin_delete_role(role_uuid: UUID, auth=Cookie(None)):
|
async def admin_delete_role(org_uuid: UUID, role_uuid: UUID, auth=Cookie(None)):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(
|
||||||
role = await db.instance.get_role(role_uuid)
|
auth, ["auth:admin", f"auth:org:{org_uuid}"], match=permutil.has_any
|
||||||
if not ctx or not (
|
|
||||||
permutil.has_any(ctx, ["auth:admin"])
|
|
||||||
or (
|
|
||||||
permutil.has_any(ctx, [f"auth:org:{role.org_uuid}"])
|
|
||||||
and getattr(ctx.org, "uuid", None) == role.org_uuid
|
|
||||||
)
|
)
|
||||||
):
|
role = await db.instance.get_role(role_uuid)
|
||||||
raise ValueError("Insufficient permissions")
|
if role.org_uuid != org_uuid:
|
||||||
|
raise HTTPException(status_code=404, detail="Role not found in organization")
|
||||||
await db.instance.delete_role(role_uuid)
|
await db.instance.delete_role(role_uuid)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
@ -249,15 +211,9 @@ async def admin_delete_role(role_uuid: UUID, auth=Cookie(None)):
|
|||||||
async def admin_create_user(
|
async def admin_create_user(
|
||||||
org_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
org_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
||||||
):
|
):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(
|
||||||
if not ctx or not (
|
auth, ["auth:admin", f"auth:org:{org_uuid}"], match=permutil.has_any
|
||||||
permutil.has_any(ctx, ["auth:admin"])
|
|
||||||
or (
|
|
||||||
permutil.has_any(ctx, [f"auth:org:{org_uuid}"])
|
|
||||||
and getattr(ctx.org, "uuid", None) == org_uuid
|
|
||||||
)
|
)
|
||||||
):
|
|
||||||
raise ValueError("Insufficient permissions")
|
|
||||||
display_name = payload.get("display_name")
|
display_name = payload.get("display_name")
|
||||||
role_name = payload.get("role")
|
role_name = payload.get("role")
|
||||||
if not display_name or not role_name:
|
if not display_name or not role_name:
|
||||||
@ -284,15 +240,9 @@ async def admin_create_user(
|
|||||||
async def admin_update_user_role(
|
async def admin_update_user_role(
|
||||||
org_uuid: UUID, user_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
org_uuid: UUID, user_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
||||||
):
|
):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(
|
||||||
if not ctx or not (
|
auth, ["auth:admin", f"auth:org:{org_uuid}"], match=permutil.has_any
|
||||||
permutil.has_any(ctx, ["auth:admin"])
|
|
||||||
or (
|
|
||||||
permutil.has_any(ctx, [f"auth:org:{org_uuid}"])
|
|
||||||
and getattr(ctx.org, "uuid", None) == org_uuid
|
|
||||||
)
|
)
|
||||||
):
|
|
||||||
raise ValueError("Insufficient permissions")
|
|
||||||
new_role = payload.get("role")
|
new_role = payload.get("role")
|
||||||
if not new_role:
|
if not new_role:
|
||||||
raise ValueError("role is required")
|
raise ValueError("role is required")
|
||||||
@ -309,19 +259,22 @@ async def admin_update_user_role(
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/users/{user_uuid}/create-link")
|
@app.post("/orgs/{org_uuid}/users/{user_uuid}/create-link")
|
||||||
async def admin_create_user_registration_link(user_uuid: UUID, auth=Cookie(None)):
|
async def admin_create_user_registration_link(
|
||||||
ctx = await permutil.session_context(auth)
|
org_uuid: UUID, user_uuid: UUID, auth=Cookie(None)
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
user_org, _role_name = await db.instance.get_user_organization(user_uuid)
|
user_org, _role_name = await db.instance.get_user_organization(user_uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
if not ctx or not (
|
if user_org.uuid != org_uuid:
|
||||||
permutil.has_any(ctx, ["auth:admin"])
|
raise HTTPException(status_code=404, detail="User not found in organization")
|
||||||
or (
|
ctx = await authz.verify(
|
||||||
permutil.has_any(ctx, [f"auth:org:{user_org.uuid}"])
|
auth, ["auth:admin", f"auth:org:{org_uuid}"], match=permutil.has_any
|
||||||
and getattr(ctx.org, "uuid", None) == user_org.uuid
|
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
"auth:admin" not in ctx.role.permissions
|
||||||
|
and f"auth:org:{org_uuid}" not in ctx.role.permissions
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||||
token = passphrase.generate()
|
token = passphrase.generate()
|
||||||
@ -336,19 +289,20 @@ async def admin_create_user_registration_link(user_uuid: UUID, auth=Cookie(None)
|
|||||||
return {"url": url, "expires": expires().isoformat()}
|
return {"url": url, "expires": expires().isoformat()}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/users/{user_uuid}")
|
@app.get("/orgs/{org_uuid}/users/{user_uuid}")
|
||||||
async def admin_get_user_detail(user_uuid: UUID, auth=Cookie(None)):
|
async def admin_get_user_detail(org_uuid: UUID, user_uuid: UUID, auth=Cookie(None)):
|
||||||
ctx = await permutil.session_context(auth)
|
|
||||||
try:
|
try:
|
||||||
user_org, role_name = await db.instance.get_user_organization(user_uuid)
|
user_org, role_name = await db.instance.get_user_organization(user_uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
if not ctx or not (
|
if user_org.uuid != org_uuid:
|
||||||
permutil.has_any(ctx, ["auth:admin"])
|
raise HTTPException(status_code=404, detail="User not found in organization")
|
||||||
or (
|
ctx = await authz.verify(
|
||||||
permutil.has_any(ctx, [f"auth:org:{user_org.uuid}"])
|
auth, ["auth:admin", f"auth:org:{org_uuid}"], match=permutil.has_any
|
||||||
and getattr(ctx.org, "uuid", None) == user_org.uuid
|
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
"auth:admin" not in ctx.role.permissions
|
||||||
|
and f"auth:org:{org_uuid}" not in ctx.role.permissions
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||||
user = await db.instance.get_user_by_uuid(user_uuid)
|
user = await db.instance.get_user_by_uuid(user_uuid)
|
||||||
@ -389,21 +343,22 @@ async def admin_get_user_detail(user_uuid: UUID, auth=Cookie(None)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.put("/users/{user_uuid}/display-name")
|
@app.put("/orgs/{org_uuid}/users/{user_uuid}/display-name")
|
||||||
async def admin_update_user_display_name(
|
async def admin_update_user_display_name(
|
||||||
user_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
org_uuid: UUID, user_uuid: UUID, payload: dict = Body(...), auth=Cookie(None)
|
||||||
):
|
):
|
||||||
ctx = await permutil.session_context(auth)
|
|
||||||
try:
|
try:
|
||||||
user_org, _role_name = await db.instance.get_user_organization(user_uuid)
|
user_org, _role_name = await db.instance.get_user_organization(user_uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
if not ctx or not (
|
if user_org.uuid != org_uuid:
|
||||||
permutil.has_any(ctx, ["auth:admin"])
|
raise HTTPException(status_code=404, detail="User not found in organization")
|
||||||
or (
|
ctx = await authz.verify(
|
||||||
permutil.has_any(ctx, [f"auth:org:{user_org.uuid}"])
|
auth, ["auth:admin", f"auth:org:{org_uuid}"], match=permutil.has_any
|
||||||
and getattr(ctx.org, "uuid", None) == user_org.uuid
|
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
"auth:admin" not in ctx.role.permissions
|
||||||
|
and f"auth:org:{org_uuid}" not in ctx.role.permissions
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||||
new_name = (payload.get("display_name") or "").strip()
|
new_name = (payload.get("display_name") or "").strip()
|
||||||
@ -420,18 +375,14 @@ async def admin_update_user_display_name(
|
|||||||
|
|
||||||
@app.get("/permissions")
|
@app.get("/permissions")
|
||||||
async def admin_list_permissions(auth=Cookie(None)):
|
async def admin_list_permissions(auth=Cookie(None)):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(auth, ["auth:admin"], match=permutil.has_any)
|
||||||
if not ctx or not permutil.has_any(ctx, ["auth:admin", "auth:org:*"]):
|
|
||||||
raise ValueError("Insufficient permissions")
|
|
||||||
perms = await db.instance.list_permissions()
|
perms = await db.instance.list_permissions()
|
||||||
return [{"id": p.id, "display_name": p.display_name} for p in perms]
|
return [{"id": p.id, "display_name": p.display_name} for p in perms]
|
||||||
|
|
||||||
|
|
||||||
@app.post("/permissions")
|
@app.post("/permissions")
|
||||||
async def admin_create_permission(payload: dict = Body(...), auth=Cookie(None)):
|
async def admin_create_permission(payload: dict = Body(...), auth=Cookie(None)):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(auth, ["auth:admin"])
|
||||||
if not ctx or not permutil.has_any(ctx, ["auth:admin"]):
|
|
||||||
raise ValueError("Global admin required")
|
|
||||||
from ..db import Permission as PermDC
|
from ..db import Permission as PermDC
|
||||||
|
|
||||||
perm_id = payload.get("id")
|
perm_id = payload.get("id")
|
||||||
@ -447,9 +398,7 @@ async def admin_create_permission(payload: dict = Body(...), auth=Cookie(None)):
|
|||||||
async def admin_update_permission(
|
async def admin_update_permission(
|
||||||
permission_id: str, display_name: str, auth=Cookie(None)
|
permission_id: str, display_name: str, auth=Cookie(None)
|
||||||
):
|
):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(auth, ["auth:admin"])
|
||||||
if not ctx or not permutil.has_any(ctx, ["auth:admin"]):
|
|
||||||
raise ValueError("Global admin required")
|
|
||||||
from ..db import Permission as PermDC
|
from ..db import Permission as PermDC
|
||||||
|
|
||||||
if not display_name:
|
if not display_name:
|
||||||
@ -463,9 +412,7 @@ async def admin_update_permission(
|
|||||||
|
|
||||||
@app.post("/permission/rename")
|
@app.post("/permission/rename")
|
||||||
async def admin_rename_permission(payload: dict = Body(...), auth=Cookie(None)):
|
async def admin_rename_permission(payload: dict = Body(...), auth=Cookie(None)):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(auth, ["auth:admin"])
|
||||||
if not ctx or not permutil.has_any(ctx, ["auth:admin"]):
|
|
||||||
raise ValueError("Global admin required")
|
|
||||||
old_id = payload.get("old_id")
|
old_id = payload.get("old_id")
|
||||||
new_id = payload.get("new_id")
|
new_id = payload.get("new_id")
|
||||||
display_name = payload.get("display_name")
|
display_name = payload.get("display_name")
|
||||||
@ -485,9 +432,7 @@ async def admin_rename_permission(payload: dict = Body(...), auth=Cookie(None)):
|
|||||||
|
|
||||||
@app.delete("/permission")
|
@app.delete("/permission")
|
||||||
async def admin_delete_permission(permission_id: str, auth=Cookie(None)):
|
async def admin_delete_permission(permission_id: str, auth=Cookie(None)):
|
||||||
ctx = await permutil.session_context(auth)
|
await authz.verify(auth, ["auth:admin"])
|
||||||
if not ctx or not permutil.has_any(ctx, ["auth:admin"]):
|
|
||||||
raise ValueError("Global admin required")
|
|
||||||
querysafe.assert_safe(permission_id, field="permission_id")
|
querysafe.assert_safe(permission_id, field="permission_id")
|
||||||
await db.instance.delete_permission(permission_id)
|
await db.instance.delete_permission(permission_id)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user