Change PUT to PATCH for intent-based updates, avoiding override of fields not intended to change. This preserves role permissions matrix even if the permission is temporarily removed from the org.

This commit is contained in:
2026-01-23 15:41:23 +00:00
parent 2c783498a4
commit c13044c085
9 changed files with 279 additions and 123 deletions
+10 -12
View File
@@ -225,7 +225,7 @@ async function moveUserToRole(org, user, targetRoleDisplayName) {
if (user.role === targetRoleDisplayName) return
try {
await apiJson(`/auth/api/admin/orgs/${org.uuid}/users/${user.uuid}/role`, {
method: 'PUT',
method: 'PATCH',
body: { role: targetRoleDisplayName }
})
await loadOrgs()
@@ -272,19 +272,17 @@ function deleteRole(role) {
}
async function toggleRolePermission(role, pid, checked) {
// Calculate new permissions array
// Optimistic update
const prevPermissions = [...role.permissions]
const newPermissions = checked
? [...role.permissions, pid]
: role.permissions.filter(p => p !== pid)
// Optimistic update
const prevPermissions = [...role.permissions]
role.permissions = newPermissions
try {
await apiJson(`/auth/api/admin/orgs/${role.org_uuid}/roles/${role.uuid}`, {
method: 'PUT',
body: { display_name: role.display_name, permissions: newPermissions }
const method = checked ? 'POST' : 'DELETE'
await apiJson(`/auth/api/admin/orgs/${role.org_uuid}/roles/${role.uuid}/permissions/${pid}`, {
method
})
await loadOrgs()
} catch (e) {
@@ -564,7 +562,7 @@ async function submitDialog() {
// Close dialog immediately, then perform async operation
closeDialog()
apiJson(`/auth/api/admin/orgs/${org.uuid}`, { method: 'PUT', body: { display_name: name, permissions: org.permissions } })
apiJson(`/auth/api/admin/orgs/${org.uuid}`, { method: 'PATCH', body: { display_name: name } })
.then(() => {
authStore.showMessage(`Organization renamed to "${name}".`, 'success', 2500)
loadOrgs()
@@ -592,7 +590,7 @@ async function submitDialog() {
// Close dialog immediately, then perform async operation
closeDialog()
apiJson(`/auth/api/admin/orgs/${role.org_uuid}/roles/${role.uuid}`, { method: 'PUT', body: { display_name: name, permissions: role.permissions } })
apiJson(`/auth/api/admin/orgs/${role.org_uuid}/roles/${role.uuid}`, { method: 'PATCH', body: { display_name: name } })
.then(() => {
authStore.showMessage(`Role renamed to "${name}".`, 'success', 2500)
loadOrgs()
@@ -620,7 +618,7 @@ async function submitDialog() {
// Close dialog immediately, then perform async operation
closeDialog()
apiJson(`/auth/api/admin/orgs/${user.org_uuid}/users/${user.uuid}/display-name`, { method: 'PUT', body: { display_name: name } })
apiJson(`/auth/api/admin/orgs/${user.org_uuid}/users/${user.uuid}/display-name`, { method: 'PATCH', body: { display_name: name } })
.then(() => {
authStore.showMessage(`User renamed to "${name}".`, 'success', 2500)
onUserNameSaved()
@@ -649,7 +647,7 @@ async function submitDialog() {
// Display name or domain changed
const params = new URLSearchParams({ permission_id: permission.scope, display_name: newDisplay })
if (newDomain) params.set('domain', newDomain)
apiCall = apiJson(`/auth/api/admin/permission?${params.toString()}`, { method: 'PUT' })
apiCall = apiJson(`/auth/api/admin/permission?${params.toString()}`, { method: 'PATCH' })
} else {
// No changes
return
+1 -1
View File
@@ -336,7 +336,7 @@ const saveName = async () => {
if (!name) { authStore.showMessage('Name cannot be empty', 'error'); return }
try {
saving.value = true
await apiJson('/auth/api/user/display-name', { method: 'PUT', body: { display_name: name } })
await apiJson('/auth/api/user/display-name', { method: 'PATCH', body: { display_name: name } })
showNameDialog.value = false
await authStore.loadUserInfo()
authStore.showMessage('Name updated successfully!', 'success', 3000)
+101 -6
View File
@@ -468,14 +468,32 @@ class DB:
visits=u.visits,
)
def _build_role(self, role_uuid: str) -> Role:
"""Build a Role object from internal storage. Caller must hold lock."""
def _build_role(self, role_uuid: str, org_filter: bool = False) -> Role:
"""Build a Role object from internal storage. Caller must hold lock.
Args:
role_uuid: The role UUID string
org_filter: If True, filter permissions to only those the org can grant
"""
r = self._data.roles[role_uuid]
permissions = list(r.permissions)
# Filter by org if requested
if org_filter:
org_uuid = r.org
if org_uuid in self._data.orgs:
org_allowed_scopes = {
p.scope
for pid, p in self._data.permissions.items()
if org_uuid in p.orgs
}
permissions = [p for p in permissions if p in org_allowed_scopes]
return Role(
uuid=UUID(role_uuid),
org_uuid=UUID(r.org),
display_name=r.display_name,
permissions=list(r.permissions),
permissions=permissions,
)
def _build_org(self, org_uuid: str, include_roles: bool = False) -> Org:
@@ -491,8 +509,9 @@ class DB:
permissions=perm_scopes,
)
if include_roles:
# When building roles for org display, filter by what org can grant
org.roles = [
self._build_role(role_uuid)
self._build_role(role_uuid, org_filter=True)
for role_uuid, r in self._data.roles.items()
if r.org == org_uuid
]
@@ -582,6 +601,16 @@ class DB:
{p: True for p in role.permissions} if role.permissions else {}
)
def update_role_name(
self, role_uuid: UUID, display_name: str, actor: str = "system"
) -> None:
"""Update only the role display name (intent-based API)."""
with self.session(actor):
key = str(role_uuid)
if key not in self._data.roles:
raise ValueError("Role not found")
self._data.roles[key].display_name = display_name
def delete_role(self, role_uuid: UUID, actor: str = "system") -> None:
with self.session(actor):
key = str(role_uuid)
@@ -599,6 +628,37 @@ class DB:
raise ValueError("Role not found")
return self._build_role(key)
def get_role_hidden_permissions(self, role_uuid: UUID) -> list[str]:
"""Get permission scopes assigned to role but not grantable by its org.
These are "hidden" permissions that should be preserved when updating
the role, so they can become effective again if the org regains access.
"""
with self._lock:
key = str(role_uuid)
if key not in self._data.roles:
return []
role_data = self._data.roles[key]
org_uuid = role_data.org
# Get org's grantable scopes
if org_uuid not in self._data.orgs:
return []
org_allowed_scopes = {
p.scope
for pid, p in self._data.permissions.items()
if org_uuid in p.orgs
}
# Return scopes in role but not in org
return [
scope
for scope in role_data.permissions
if scope not in org_allowed_scopes
]
# -------------------------------------------------------------------------
# Credential operations
# -------------------------------------------------------------------------
@@ -844,6 +904,16 @@ class DB:
p.orgs[key] = True
break
def update_organization_name(
self, org_uuid: UUID, display_name: str, actor: str = "system"
) -> None:
"""Update only the organization display name (intent-based API)."""
with self.session(actor):
key = str(org_uuid)
if key not in self._data.orgs:
raise ValueError("Organization not found")
self._data.orgs[key].display_name = display_name
def delete_organization(self, org_uuid: UUID, actor: str = "system") -> None:
with self.session(actor):
key = str(org_uuid)
@@ -1189,18 +1259,43 @@ class DB:
elif permission_id in self._data.roles[key].permissions:
del self._data.roles[key].permissions[permission_id]
def get_role_permissions(self, role_uuid: UUID) -> list[Permission]:
def get_role_permissions(
self, role_uuid: UUID, filter_by_org: bool = True
) -> list[Permission]:
"""Get permissions granted by a role.
Note: Roles store scopes, so we need to look up permissions by scope.
Args:
role_uuid: The role UUID
filter_by_org: If True, only return permissions that the role's org
can grant. Set to False to see all assigned permissions
regardless of org restrictions.
"""
with self._lock:
key = str(role_uuid)
if key not in self._data.roles:
return []
scopes = list(self._data.roles[key].permissions.keys())
role_data = self._data.roles[key]
scopes = list(role_data.permissions.keys())
# Get org permissions if filtering
org_allowed_scopes = None
if filter_by_org:
org_uuid = role_data.org
if org_uuid in self._data.orgs:
org_allowed_scopes = {
p.scope
for pid, p in self._data.permissions.items()
if org_uuid in p.orgs
}
permissions = []
for scope in scopes:
# Skip if org filtering is enabled and scope not allowed by org
if org_allowed_scopes is not None and scope not in org_allowed_scopes:
continue
# Find permission with this scope
for pid, p in self._data.permissions.items():
if p.scope == scope:
+100 -54
View File
@@ -25,7 +25,12 @@ app = FastAPI()
def is_global_admin(ctx) -> bool:
"""Check if user has global admin permission."""
return "auth:admin" in ctx.role.permissions
effective_scopes = (
{p.scope for p in (ctx.permissions or [])}
if ctx.permissions
else set(ctx.role.permissions or [])
)
return "auth:admin" in effective_scopes
def is_org_admin(ctx, org_uuid: UUID | None = None) -> bool:
@@ -34,7 +39,12 @@ def is_org_admin(ctx, org_uuid: UUID | None = None) -> bool:
If org_uuid is provided, checks if user is admin of that specific org.
If org_uuid is None, checks if user is admin of their own org.
"""
if "auth:org:admin" not in ctx.role.permissions:
effective_scopes = (
{p.scope for p in (ctx.permissions or [])}
if ctx.permissions
else set(ctx.role.permissions or [])
)
if "auth:org:admin" not in effective_scopes:
return False
if org_uuid is None:
return True
@@ -148,13 +158,14 @@ async def admin_create_org(
return {"uuid": str(org_uuid)}
@app.put("/orgs/{org_uuid}")
async def admin_update_org(
@app.patch("/orgs/{org_uuid}")
async def admin_update_org_name(
org_uuid: UUID,
request: Request,
payload: dict = Body(...),
auth=AUTH_COOKIE,
):
"""Update organization display name only."""
ctx = await authz.verify(
auth,
["auth:admin", "auth:org:admin"],
@@ -165,26 +176,11 @@ async def admin_update_org(
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
from ..db import Org as OrgDC # local import to avoid cycles
display_name = payload.get("display_name")
if not display_name:
raise ValueError("display_name is required")
current = db.get_organization(str(org_uuid))
display_name = payload.get("display_name") or current.display_name
permissions = payload.get("permissions")
if permissions is None:
permissions = current.permissions or []
# Sanity check: prevent removing permissions that would break current user's admin access
org_admin_perm = "auth:org:admin"
# If current user is org admin (not global admin), ensure org admin perm remains
if not is_global_admin(ctx) and is_org_admin(ctx, org_uuid):
if org_admin_perm not in permissions:
raise ValueError(
"Cannot remove organization admin permission from your own organization"
)
org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions)
db.update_organization(org)
db.update_organization_name(org_uuid, display_name)
return {"status": "ok"}
@@ -301,15 +297,15 @@ async def admin_create_role(
return {"uuid": str(role_uuid)}
@app.put("/orgs/{org_uuid}/roles/{role_uuid}")
async def admin_update_role(
@app.patch("/orgs/{org_uuid}/roles/{role_uuid}")
async def admin_update_role_name(
org_uuid: UUID,
role_uuid: UUID,
request: Request,
payload: dict = Body(...),
auth=AUTH_COOKIE,
):
# Verify caller is global admin or admin of provided org
"""Update role display name only."""
ctx = await authz.verify(
auth,
["auth:admin", "auth:org:admin"],
@@ -323,35 +319,85 @@ async def admin_update_role(
role = db.get_role(role_uuid)
if role.org_uuid != org_uuid:
raise HTTPException(status_code=404, detail="Role not found in organization")
from ..db import Role as RoleDC
display_name = payload.get("display_name") or role.display_name
permissions = payload.get("permissions")
if permissions is None:
permissions = role.permissions
org = db.get_organization(str(org_uuid))
grantable = set(org.permissions or [])
existing_permissions = set(role.permissions)
for pid in permissions:
db.get_permission(pid)
if pid not in existing_permissions and pid not in grantable:
raise ValueError(f"Permission not grantable by org: {pid}")
display_name = payload.get("display_name")
if not display_name:
raise ValueError("display_name is required")
# Sanity check: prevent admin from removing their own access via role update
if ctx.org.uuid == org_uuid and ctx.role.uuid == role_uuid:
has_admin_access = (
"auth:admin" in permissions or "auth:org:admin" in permissions
)
if not has_admin_access:
raise ValueError("Cannot update your own role to remove admin permissions")
db.update_role_name(role_uuid, display_name)
return {"status": "ok"}
updated = RoleDC(
uuid=role_uuid,
org_uuid=org_uuid,
display_name=display_name,
permissions=permissions,
@app.post("/orgs/{org_uuid}/roles/{role_uuid}/permissions/{permission_id}")
async def admin_add_role_permission(
org_uuid: UUID,
role_uuid: UUID,
permission_id: str,
request: Request,
auth=AUTH_COOKIE,
):
"""Add a permission to a role (intent-based API)."""
ctx = await authz.verify(
auth,
["auth:admin", "auth:org:admin"],
match=permutil.has_any,
host=request.headers.get("host"),
)
db.update_role(updated)
if not can_manage_org(ctx, org_uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
role = db.get_role(role_uuid)
if role.org_uuid != org_uuid:
raise HTTPException(status_code=404, detail="Role not found in organization")
# Verify permission exists and org can grant it
db.get_permission(permission_id)
org = db.get_organization(str(org_uuid))
if permission_id not in org.permissions:
raise ValueError(f"Permission not grantable by organization")
db.add_permission_to_role(role_uuid, permission_id)
return {"status": "ok"}
@app.delete("/orgs/{org_uuid}/roles/{role_uuid}/permissions/{permission_id}")
async def admin_remove_role_permission(
org_uuid: UUID,
role_uuid: UUID,
permission_id: str,
request: Request,
auth=AUTH_COOKIE,
):
"""Remove a permission from a role (intent-based API)."""
ctx = await authz.verify(
auth,
["auth:admin", "auth:org:admin"],
match=permutil.has_any,
host=request.headers.get("host"),
)
if not can_manage_org(ctx, org_uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
role = db.get_role(role_uuid)
if role.org_uuid != org_uuid:
raise HTTPException(status_code=404, detail="Role not found in organization")
# Sanity check: prevent admin from removing their own access
if ctx.org.uuid == org_uuid and ctx.role.uuid == role_uuid:
if permission_id in ["auth:admin", "auth:org:admin"]:
# Check if removing this permission would leave no admin access
remaining_perms = set(role.permissions) - {permission_id}
if (
"auth:admin" not in remaining_perms
and "auth:org:admin" not in remaining_perms
):
raise ValueError("Cannot remove your own admin permissions")
db.remove_permission_from_role(role_uuid, permission_id)
return {"status": "ok"}
@@ -427,7 +473,7 @@ async def admin_create_user(
return {"uuid": str(user_uuid)}
@app.put("/orgs/{org_uuid}/users/{user_uuid}/role")
@app.patch("/orgs/{org_uuid}/users/{user_uuid}/role")
async def admin_update_user_role(
org_uuid: UUID,
user_uuid: UUID,
@@ -668,7 +714,7 @@ async def admin_get_user_detail(
}
@app.put("/orgs/{org_uuid}/users/{user_uuid}/display-name")
@app.patch("/orgs/{org_uuid}/users/{user_uuid}/display-name")
async def admin_update_user_display_name(
org_uuid: UUID,
user_uuid: UUID,
@@ -923,7 +969,7 @@ async def admin_create_permission(
return {"status": "ok"}
@app.put("/permission")
@app.patch("/permission")
async def admin_update_permission(
request: Request,
auth=AUTH_COOKIE,
+7 -2
View File
@@ -94,14 +94,19 @@ async def verify(
if not match(ctx, perm):
# Determine which permissions are missing for clearer diagnostics
missing = sorted(set(perm) - set(ctx.role.permissions))
effective_scopes = (
{p.scope for p in (ctx.permissions or [])}
if ctx.permissions
else set(ctx.role.permissions or [])
)
missing = sorted(set(perm) - effective_scopes)
logger.warning(
"Permission denied: user=%s role=%s missing=%s required=%s granted=%s", # noqa: E501
getattr(ctx.user, "uuid", "?"),
getattr(ctx.role, "display_name", "?"),
missing,
perm,
ctx.role.permissions,
list(effective_scopes),
)
raise AuthException(
status_code=403, mode="forbidden", detail="Permission required"
+1 -1
View File
@@ -33,7 +33,7 @@ async def auth_exception_handler(_request, exc: authz.AuthException):
)
@app.put("/display-name")
@app.patch("/display-name")
async def user_update_display_name(
request: Request,
response: Response,
+14 -2
View File
@@ -17,12 +17,24 @@ def _match(perms: set[str], patterns: Sequence[str]):
)
def _get_effective_scopes(ctx) -> set[str]:
"""Get effective permission scopes from context.
Returns scopes from ctx.permissions (filtered by org) if available,
otherwise falls back to ctx.role.permissions for backwards compatibility.
"""
if ctx.permissions:
return {p.scope for p in ctx.permissions}
# Fallback for contexts without effective permissions computed
return set(ctx.role.permissions or [])
def has_any(ctx, patterns: Sequence[str]) -> bool:
return any(_match(ctx.role.permissions, patterns)) if ctx else False
return any(_match(_get_effective_scopes(ctx), patterns)) if ctx else False
def has_all(ctx, patterns: Sequence[str]) -> bool:
return all(_match(ctx.role.permissions, patterns)) if ctx else False
return all(_match(_get_effective_scopes(ctx), patterns)) if ctx else False
async def session_context(auth: str | None, host: str | None = None):
+40 -40
View File
@@ -338,7 +338,7 @@ class TestAdminOrganizations:
self, client: httpx.AsyncClient, session_token: str, test_org
):
"""Admin should be able to update an organization."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}",
json={"display_name": "Updated Org Name"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -355,11 +355,10 @@ class TestAdminOrganizations:
test_org,
):
"""Org admin should be able to update their organization."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}",
json={
"display_name": "Org Admin Updated Name",
"permissions": ["auth:org:admin"], # Keep org admin perm
},
headers={**auth_headers(org_admin_session_token), "Host": "localhost:4401"},
)
@@ -378,18 +377,13 @@ class TestAdminOrganizations:
"""Org admin cannot remove their org admin permission from org's permissions."""
# The auth:org:admin perm is already created and added by org_admin_permission fixture
# Try to remove all permissions including org admin perm
response = await client.put(
f"/auth/api/admin/orgs/{test_org.uuid}",
json={
"display_name": "Try Remove Own Perm",
"permissions": [], # Remove org admin perm from org's permissions
},
# Try to remove org admin perm (this is validated server-side in the remove endpoint)
response = await client.delete(
f"/auth/api/admin/orgs/{test_org.uuid}/permission?permission_id=auth:org:admin",
headers={**auth_headers(org_admin_session_token), "Host": "localhost:4401"},
)
assert response.status_code == 400
data = response.json()
assert "Cannot remove organization admin permission" in data["detail"]
# This should fail because only global admin can remove perms from org
assert response.status_code == 403
@pytest.mark.asyncio
async def test_delete_org_own_org_fails(
@@ -626,7 +620,7 @@ class TestAdminRoles:
self, client: httpx.AsyncClient, session_token: str, test_org, test_role
):
"""Admin should be able to update a role."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{test_role.uuid}",
json={"display_name": "Updated Role Name"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -640,7 +634,7 @@ class TestAdminRoles:
self, client: httpx.AsyncClient, session_token: str, test_org, second_org_role
):
"""Cannot update role from another org."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{second_org_role.uuid}",
json={"display_name": "Try Update Wrong Org"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -659,9 +653,8 @@ class TestAdminRoles:
grantable_permission,
):
"""Admin should be able to add grantable permissions to role."""
response = await client.put(
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{user_role.uuid}",
json={"permissions": [grantable_permission.scope]},
response = await client.post(
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{user_role.uuid}/permissions/{grantable_permission.scope}",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 200
@@ -685,9 +678,8 @@ class TestAdminRoles:
)
test_db.create_permission(perm)
response = await client.put(
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{user_role.uuid}",
json={"permissions": ["test:not:grantable:update"]},
response = await client.post(
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{user_role.uuid}/permissions/test:not:grantable:update",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 400
@@ -699,14 +691,22 @@ class TestAdminRoles:
self, client: httpx.AsyncClient, session_token: str, test_org, test_role
):
"""Admin cannot remove their own admin permissions."""
response = await client.put(
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{test_role.uuid}",
json={"permissions": []}, # Remove all permissions
# test_role has both auth:admin and auth:org:admin
# Remove auth:admin first (should succeed since org:admin remains)
response = await client.delete(
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{test_role.uuid}/permissions/auth:admin",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 200
# Now try to remove auth:org:admin (should fail - would leave no admin access)
response = await client.delete(
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{test_role.uuid}/permissions/auth:org:admin",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 400
data = response.json()
assert "Cannot update your own role" in data["detail"]
assert "Cannot remove your own admin permissions" in data["detail"]
@pytest.mark.asyncio
async def test_delete_role(
@@ -863,7 +863,7 @@ class TestAdminUsersInOrg:
self, client: httpx.AsyncClient, session_token: str, test_org, test_user
):
"""Admin should be able to update user display name."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/display-name",
json={"display_name": "Updated Admin Name"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -876,7 +876,7 @@ class TestAdminUsersInOrg:
):
"""Updating non-existent user should return 404."""
fake_uuid = uuid7.create()
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{fake_uuid}/display-name",
json={"display_name": "New Name"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -890,7 +890,7 @@ class TestAdminUsersInOrg:
self, client: httpx.AsyncClient, session_token: str, test_org, second_org_user
):
"""Updating user from another org should return 404."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{second_org_user.uuid}/display-name",
json={"display_name": "New Name"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -902,7 +902,7 @@ class TestAdminUsersInOrg:
self, client: httpx.AsyncClient, session_token: str, test_org, test_user
):
"""Updating user with empty display name should fail."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/display-name",
json={"display_name": " "},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -916,7 +916,7 @@ class TestAdminUsersInOrg:
self, client: httpx.AsyncClient, session_token: str, test_org, test_user
):
"""Updating user with too long display name should fail."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/display-name",
json={"display_name": "x" * 100},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -936,7 +936,7 @@ class TestAdminUsersInOrg:
):
"""Admin should be able to change user's role within org."""
# Use regular_user who is in the same org but not the session owner
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{regular_user.uuid}/role",
json={"role": user_role.display_name},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -948,7 +948,7 @@ class TestAdminUsersInOrg:
self, client: httpx.AsyncClient, session_token: str, test_org, test_user
):
"""Updating user role without specifying role should fail."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/role",
json={},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -963,7 +963,7 @@ class TestAdminUsersInOrg:
):
"""Updating role for non-existent user should fail."""
fake_uuid = uuid7.create()
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{fake_uuid}/role",
json={"role": "User Role"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -977,7 +977,7 @@ class TestAdminUsersInOrg:
self, client: httpx.AsyncClient, session_token: str, test_org, second_org_user
):
"""Updating role for user in another org should fail."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{second_org_user.uuid}/role",
json={"role": "User Role"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -991,7 +991,7 @@ class TestAdminUsersInOrg:
self, client: httpx.AsyncClient, session_token: str, test_org, test_user
):
"""Updating user to non-existent role should fail."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/role",
json={"role": "Nonexistent Role"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -1010,7 +1010,7 @@ class TestAdminUsersInOrg:
user_role,
):
"""Admin cannot change their own role to non-admin role."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{org_admin_user.uuid}/role",
json={"role": user_role.display_name},
headers={**auth_headers(org_admin_session_token), "Host": "localhost:4401"},
@@ -1031,7 +1031,7 @@ class TestAdminUsersInOrg:
"""Admin can change their own role to another admin role."""
# test_user is already on test_role which has auth:admin
# Changing to the same role should succeed (no permission loss)
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/role",
json={"role": test_role.display_name},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -1382,7 +1382,7 @@ class TestAdminPermissions:
)
test_db.create_permission(perm)
response = await client.put(
response = await client.patch(
"/auth/api/admin/permission?permission_id=test:updateable&display_name=Updated%20Name",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
@@ -1403,7 +1403,7 @@ class TestAdminPermissions:
)
test_db.create_permission(perm)
response = await client.put(
response = await client.patch(
"/auth/api/admin/permission?permission_id=test:perm&display_name=",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
@@ -1626,7 +1626,7 @@ class TestOrgAdminAuthExceptions:
test_user,
):
"""Regular user trying to update display name should get 403."""
response = await client.put(
response = await client.patch(
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/display-name",
json={"display_name": "New Name"},
headers={**auth_headers(regular_session_token), "Host": "localhost:4401"},
+5 -5
View File
@@ -16,12 +16,12 @@ from tests.conftest import auth_headers
class TestUserDisplayName:
"""Tests for PUT /auth/api/user/display-name"""
"""Tests for PATCH /auth/api/user/display-name"""
@pytest.mark.asyncio
async def test_update_display_name_requires_auth(self, client: httpx.AsyncClient):
"""Update display name without auth should return 401."""
response = await client.put(
response = await client.patch(
"/auth/api/user/display-name",
json={"display_name": "New Name"},
)
@@ -32,7 +32,7 @@ class TestUserDisplayName:
self, client: httpx.AsyncClient, session_token: str
):
"""User should be able to update their display name."""
response = await client.put(
response = await client.patch(
"/auth/api/user/display-name",
json={"display_name": "Updated Name"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -46,7 +46,7 @@ class TestUserDisplayName:
self, client: httpx.AsyncClient, session_token: str
):
"""Empty display name should fail."""
response = await client.put(
response = await client.patch(
"/auth/api/user/display-name",
json={"display_name": ""},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
@@ -59,7 +59,7 @@ class TestUserDisplayName:
):
"""Display name over 64 chars should fail."""
long_name = "x" * 100
response = await client.put(
response = await client.patch(
"/auth/api/user/display-name",
json={"display_name": long_name},
headers={**auth_headers(session_token), "Host": "localhost:4401"},