Only allow safe characters in permission IDs
This commit is contained in:
parent
d045e1c520
commit
2b03fa74cd
@ -17,6 +17,16 @@ const userLink = ref(null) // latest generated registration link
|
|||||||
const userLinkExpires = ref(null)
|
const userLinkExpires = ref(null)
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const addingOrgForPermission = ref(null)
|
const addingOrgForPermission = ref(null)
|
||||||
|
const PERMISSION_ID_PATTERN = '^[A-Za-z0-9:._~-]+$'
|
||||||
|
const showCreatePermission = ref(false)
|
||||||
|
const newPermId = ref('')
|
||||||
|
const newPermName = ref('')
|
||||||
|
const editingPermId = ref(null)
|
||||||
|
const renameIdValue = ref('')
|
||||||
|
const safeIdRegex = /[^A-Za-z0-9:._~-]/g
|
||||||
|
|
||||||
|
function sanitizeNewId() { if (newPermId.value) newPermId.value = newPermId.value.replace(safeIdRegex, '') }
|
||||||
|
function sanitizeRenameId() { if (renameIdValue.value) renameIdValue.value = renameIdValue.value.replace(safeIdRegex, '') }
|
||||||
|
|
||||||
function handleGlobalClick(e) {
|
function handleGlobalClick(e) {
|
||||||
if (!addingOrgForPermission.value) return
|
if (!addingOrgForPermission.value) return
|
||||||
@ -88,22 +98,22 @@ async function renamePermissionDisplay(p) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renamePermissionId(p) {
|
function startRenamePermissionId(p) {
|
||||||
const newId = prompt('New permission id', p.id)
|
editingPermId.value = p.id
|
||||||
if (!newId || newId === p.id) return
|
renameIdValue.value = p.id
|
||||||
|
}
|
||||||
|
function cancelRenameId() { editingPermId.value = null; renameIdValue.value = '' }
|
||||||
|
async function submitRenamePermissionId(p) {
|
||||||
|
const newId = renameIdValue.value.trim()
|
||||||
|
if (!newId || newId === p.id) { cancelRenameId(); return }
|
||||||
try {
|
try {
|
||||||
const body = { old_id: p.id, new_id: newId, display_name: p.display_name }
|
const body = { old_id: p.id, new_id: newId, display_name: p.display_name }
|
||||||
const res = await fetch('/auth/admin/permission/rename', {
|
const res = await fetch('/auth/admin/permission/rename', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||||
method: 'POST',
|
let data; try { data = await res.json() } catch(_) { data = {} }
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
})
|
|
||||||
let data
|
|
||||||
try { data = await res.json() } catch(_) { data = {} }
|
|
||||||
if (!res.ok || data.detail) throw new Error(data.detail || data.error || `Failed (${res.status})`)
|
if (!res.ok || data.detail) throw new Error(data.detail || data.error || `Failed (${res.status})`)
|
||||||
await refreshPermissionsContext()
|
await refreshPermissionsContext(); cancelRenameId()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert((e && e.message) ? e.message : 'Failed to rename permission id')
|
alert(e?.message || 'Failed to rename permission id')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,20 +346,15 @@ async function deleteRole(role) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Permission actions
|
// Permission actions
|
||||||
async function createPermission() {
|
async function submitCreatePermission() {
|
||||||
const id = prompt('Permission ID (e.g., auth/example):')
|
const id = newPermId.value.trim()
|
||||||
if (!id) return
|
const name = newPermName.value.trim()
|
||||||
const name = prompt('Permission display name:')
|
if (!id || !name) return
|
||||||
if (!name) return
|
const res = await fetch('/auth/admin/permissions', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, display_name: name }) })
|
||||||
const res = await fetch('/auth/admin/permissions', {
|
const data = await res.json(); if (data.detail) { alert(data.detail); return }
|
||||||
method: 'POST',
|
await loadPermissions(); newPermId.value=''; newPermName.value=''; showCreatePermission.value=false
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
body: JSON.stringify({ id, display_name: name })
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
|
||||||
if (data.detail) return alert(data.detail)
|
|
||||||
await loadPermissions()
|
|
||||||
}
|
}
|
||||||
|
function cancelCreatePermission() { newPermId.value=''; newPermName.value=''; showCreatePermission.value=false }
|
||||||
|
|
||||||
async function updatePermission(p) {
|
async function updatePermission(p) {
|
||||||
const name = prompt('Permission display name:', p.display_name)
|
const name = prompt('Permission display name:', p.display_name)
|
||||||
@ -623,7 +628,13 @@ async function toggleRolePermission(role, permId, checked) {
|
|||||||
<div v-if="!selectedUser && !selectedOrg && (info.is_global_admin || info.is_org_admin)" class="card">
|
<div v-if="!selectedUser && !selectedOrg && (info.is_global_admin || info.is_org_admin)" class="card">
|
||||||
<h2>All Permissions</h2>
|
<h2>All Permissions</h2>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button @click="createPermission">+ Create Permission</button>
|
<button v-if="!showCreatePermission" @click="showCreatePermission = true">+ Create Permission</button>
|
||||||
|
<form v-else class="inline-form" @submit.prevent="submitCreatePermission">
|
||||||
|
<input v-model="newPermId" @input="sanitizeNewId" required :pattern="PERMISSION_ID_PATTERN" placeholder="permission id" title="Allowed: A-Za-z0-9:._~-" />
|
||||||
|
<input v-model="newPermName" required placeholder="display name" />
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
<button type="button" @click="cancelCreatePermission">Cancel</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="permission-grid">
|
<div class="permission-grid">
|
||||||
<div class="perm-grid-head">Permission</div>
|
<div class="perm-grid-head">Permission</div>
|
||||||
@ -672,9 +683,16 @@ async function toggleRolePermission(role, permId, checked) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="perm-cell perm-users center">{{ permissionSummary[p.id]?.userCount || 0 }}</div>
|
<div class="perm-cell perm-users center">{{ permissionSummary[p.id]?.userCount || 0 }}</div>
|
||||||
<div class="perm-cell perm-actions center">
|
<div class="perm-cell perm-actions center">
|
||||||
<button @click="renamePermissionDisplay(p)" class="icon-btn" aria-label="Change display name" title="Change display name">✏️</button>
|
<template v-if="editingPermId !== p.id">
|
||||||
<button @click="renamePermissionId(p)" class="icon-btn" aria-label="Change id" title="Change id">🆔</button>
|
<button @click="renamePermissionDisplay(p)" class="icon-btn" aria-label="Change display name" title="Change display name">✏️</button>
|
||||||
<button @click="deletePermission(p)" class="icon-btn delete-icon" aria-label="Delete permission" title="Delete permission">❌</button>
|
<button @click="startRenamePermissionId(p)" class="icon-btn" aria-label="Change id" title="Change id">🆔</button>
|
||||||
|
<button @click="deletePermission(p)" class="icon-btn delete-icon" aria-label="Delete permission" title="Delete permission">❌</button>
|
||||||
|
</template>
|
||||||
|
<form v-else class="inline-id-form" @submit.prevent="submitRenamePermissionId(p)">
|
||||||
|
<input v-model="renameIdValue" @input="sanitizeRenameId" required :pattern="PERMISSION_ID_PATTERN" class="id-input" title="Allowed: A-Za-z0-9:._~-" />
|
||||||
|
<button type="submit" class="icon-btn" aria-label="Save">✔</button>
|
||||||
|
<button type="button" class="icon-btn" @click="cancelRenameId" aria-label="Cancel">✖</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@ from .. import aaguid
|
|||||||
from ..authsession import delete_credential, expires, get_reset, get_session
|
from ..authsession import delete_credential, expires, get_reset, get_session
|
||||||
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 tokens
|
from ..util import querysafe, tokens
|
||||||
from ..util.tokens import session_key
|
from ..util.tokens import session_key
|
||||||
from . import authz, session
|
from . import authz, session
|
||||||
|
|
||||||
@ -260,6 +260,7 @@ def register_api_routes(app: FastAPI):
|
|||||||
ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth)
|
ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth)
|
||||||
if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)):
|
if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)):
|
||||||
raise ValueError("Insufficient permissions")
|
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"}
|
||||||
|
|
||||||
@ -270,6 +271,7 @@ def register_api_routes(app: FastAPI):
|
|||||||
ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth)
|
ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth)
|
||||||
if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)):
|
if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)):
|
||||||
raise ValueError("Insufficient permissions")
|
raise ValueError("Insufficient permissions")
|
||||||
|
querysafe.assert_safe(permission_id, field="permission_id")
|
||||||
await db.instance.remove_permission_from_organization(
|
await db.instance.remove_permission_from_organization(
|
||||||
str(org_uuid), permission_id
|
str(org_uuid), permission_id
|
||||||
)
|
)
|
||||||
@ -506,6 +508,7 @@ def register_api_routes(app: FastAPI):
|
|||||||
display_name = payload.get("display_name")
|
display_name = payload.get("display_name")
|
||||||
if not perm_id or not display_name:
|
if not perm_id or not display_name:
|
||||||
raise ValueError("id and display_name are required")
|
raise ValueError("id and display_name are required")
|
||||||
|
querysafe.assert_safe(perm_id, field="id")
|
||||||
await db.instance.create_permission(
|
await db.instance.create_permission(
|
||||||
PermDC(id=perm_id, display_name=display_name)
|
PermDC(id=perm_id, display_name=display_name)
|
||||||
)
|
)
|
||||||
@ -522,6 +525,7 @@ def register_api_routes(app: FastAPI):
|
|||||||
|
|
||||||
if not display_name:
|
if not display_name:
|
||||||
raise ValueError("display_name is required")
|
raise ValueError("display_name is required")
|
||||||
|
querysafe.assert_safe(permission_id, field="permission_id")
|
||||||
await db.instance.update_permission(
|
await db.instance.update_permission(
|
||||||
PermDC(id=permission_id, display_name=display_name)
|
PermDC(id=permission_id, display_name=display_name)
|
||||||
)
|
)
|
||||||
@ -541,6 +545,8 @@ def register_api_routes(app: FastAPI):
|
|||||||
display_name = payload.get("display_name")
|
display_name = payload.get("display_name")
|
||||||
if not old_id or not new_id:
|
if not old_id or not new_id:
|
||||||
raise ValueError("old_id and new_id required")
|
raise ValueError("old_id and new_id required")
|
||||||
|
querysafe.assert_safe(old_id, field="old_id")
|
||||||
|
querysafe.assert_safe(new_id, field="new_id")
|
||||||
if display_name is None:
|
if display_name is None:
|
||||||
# Fetch old to retain display name
|
# Fetch old to retain display name
|
||||||
perm = await db.instance.get_permission(old_id)
|
perm = await db.instance.get_permission(old_id)
|
||||||
@ -557,6 +563,7 @@ def register_api_routes(app: FastAPI):
|
|||||||
_, is_global_admin, _ = await _get_ctx_and_admin_flags(auth)
|
_, is_global_admin, _ = await _get_ctx_and_admin_flags(auth)
|
||||||
if not is_global_admin:
|
if not is_global_admin:
|
||||||
raise ValueError("Global admin required")
|
raise ValueError("Global admin required")
|
||||||
|
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"}
|
||||||
|
|
||||||
|
10
passkey/util/querysafe.py
Normal file
10
passkey/util/querysafe.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
_SAFE_RE = re.compile(r"^[A-Za-z0-9:._~-]+$")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_safe(value: str, *, field: str = "value") -> None:
|
||||||
|
if not isinstance(value, str) or not value or not _SAFE_RE.match(value):
|
||||||
|
raise ValueError(f"{field} must match ^[A-Za-z0-9:._~-]+$")
|
||||||
|
|
||||||
|
__all__ = ["assert_safe"]
|
Loading…
x
Reference in New Issue
Block a user