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 authStore = useAuthStore()
|
||||
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) {
|
||||
if (!addingOrgForPermission.value) return
|
||||
@ -88,22 +98,22 @@ async function renamePermissionDisplay(p) {
|
||||
}
|
||||
}
|
||||
|
||||
async function renamePermissionId(p) {
|
||||
const newId = prompt('New permission id', p.id)
|
||||
if (!newId || newId === p.id) return
|
||||
function startRenamePermissionId(p) {
|
||||
editingPermId.value = p.id
|
||||
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 {
|
||||
const body = { old_id: p.id, new_id: newId, display_name: p.display_name }
|
||||
const res = await fetch('/auth/admin/permission/rename', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
let data
|
||||
try { data = await res.json() } catch(_) { data = {} }
|
||||
const res = await fetch('/auth/admin/permission/rename', { method: 'POST', 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})`)
|
||||
await refreshPermissionsContext()
|
||||
await refreshPermissionsContext(); cancelRenameId()
|
||||
} 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
|
||||
async function createPermission() {
|
||||
const id = prompt('Permission ID (e.g., auth/example):')
|
||||
if (!id) return
|
||||
const name = prompt('Permission display name:')
|
||||
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 data = await res.json()
|
||||
if (data.detail) return alert(data.detail)
|
||||
await loadPermissions()
|
||||
async function submitCreatePermission() {
|
||||
const id = newPermId.value.trim()
|
||||
const name = newPermName.value.trim()
|
||||
if (!id || !name) return
|
||||
const res = await fetch('/auth/admin/permissions', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, display_name: name }) })
|
||||
const data = await res.json(); if (data.detail) { alert(data.detail); return }
|
||||
await loadPermissions(); newPermId.value=''; newPermName.value=''; showCreatePermission.value=false
|
||||
}
|
||||
function cancelCreatePermission() { newPermId.value=''; newPermName.value=''; showCreatePermission.value=false }
|
||||
|
||||
async function updatePermission(p) {
|
||||
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">
|
||||
<h2>All Permissions</h2>
|
||||
<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 class="permission-grid">
|
||||
<div class="perm-grid-head">Permission</div>
|
||||
@ -672,9 +683,16 @@ async function toggleRolePermission(role, permId, checked) {
|
||||
</div>
|
||||
<div class="perm-cell perm-users center">{{ permissionSummary[p.id]?.userCount || 0 }}</div>
|
||||
<div class="perm-cell perm-actions center">
|
||||
<template v-if="editingPermId !== p.id">
|
||||
<button @click="renamePermissionDisplay(p)" class="icon-btn" aria-label="Change display name" title="Change display name">✏️</button>
|
||||
<button @click="renamePermissionId(p)" class="icon-btn" aria-label="Change id" title="Change id">🆔</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>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@ from .. import aaguid
|
||||
from ..authsession import delete_credential, expires, get_reset, get_session
|
||||
from ..globals import db
|
||||
from ..globals import passkey as global_passkey
|
||||
from ..util import tokens
|
||||
from ..util import querysafe, tokens
|
||||
from ..util.tokens import session_key
|
||||
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)
|
||||
if not (is_global_admin or (is_org_admin 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)
|
||||
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)
|
||||
if not (is_global_admin or (is_org_admin and ctx.org.uuid == 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
|
||||
)
|
||||
@ -506,6 +508,7 @@ def register_api_routes(app: FastAPI):
|
||||
display_name = payload.get("display_name")
|
||||
if not perm_id or not display_name:
|
||||
raise ValueError("id and display_name are required")
|
||||
querysafe.assert_safe(perm_id, field="id")
|
||||
await db.instance.create_permission(
|
||||
PermDC(id=perm_id, display_name=display_name)
|
||||
)
|
||||
@ -522,6 +525,7 @@ def register_api_routes(app: FastAPI):
|
||||
|
||||
if not display_name:
|
||||
raise ValueError("display_name is required")
|
||||
querysafe.assert_safe(permission_id, field="permission_id")
|
||||
await db.instance.update_permission(
|
||||
PermDC(id=permission_id, display_name=display_name)
|
||||
)
|
||||
@ -541,6 +545,8 @@ def register_api_routes(app: FastAPI):
|
||||
display_name = payload.get("display_name")
|
||||
if not old_id or not new_id:
|
||||
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:
|
||||
# Fetch old to retain display name
|
||||
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)
|
||||
if not is_global_admin:
|
||||
raise ValueError("Global admin required")
|
||||
querysafe.assert_safe(permission_id, field="permission_id")
|
||||
await db.instance.delete_permission(permission_id)
|
||||
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