CLI: positional rp-id/rp-name; init adds domains to an existing database
- 'paskia init [rp-id] [rp-name]' and 'paskia migrate [rp-id]' are now positional; comma separation and the --rp-id/--rp-name flags are gone. - With an existing paskia.kantadb, init adds the rp-id as a new domain (seeding its OIDC provider) or updates an existing domain's rp-name. - Origin allow-list semantics clarified: the bare '*' entry allows anything within the rp-id domain on any scheme and port (also the empty-list default and its display in the admin UI, replacing the synthetic '*.rp-id' row); '*.x' wildcards are https-only; exact entries match scheme, host and port. Legacy '*.rp-id' wildcards migrate to '*' to preserve their any-scheme meaning.
This commit is contained in:
@@ -490,7 +490,7 @@ function createDomain() {
|
||||
function openDomain(domain) {
|
||||
// One combined list for editing, in display order: in-domain sites and
|
||||
// related origins, classified by hostname. The default is always shown
|
||||
// explicitly as the '*.rp_id' wildcard entry.
|
||||
// explicitly as the '*' entry.
|
||||
const rows = originDisplayEntries(domain)
|
||||
openDialog('domain-edit', {
|
||||
isNew: false,
|
||||
@@ -951,6 +951,10 @@ async function submitDialog() {
|
||||
const related = {}
|
||||
for (const o of (d.origins || []).map(o => o.trim()).filter(o => o)) {
|
||||
const key = keyOf(o)
|
||||
if (key === '*') {
|
||||
origins['*'] = true // anything in-domain, any scheme/port
|
||||
continue
|
||||
}
|
||||
let hn = null
|
||||
if (key.startsWith('*.')) {
|
||||
hn = key.slice(2).replace(/\.+$/, '')
|
||||
|
||||
@@ -113,6 +113,7 @@ function isWellFormedDomain(value) {
|
||||
|
||||
function originHostname(origin) {
|
||||
if (!origin.trim()) return null
|
||||
if (origin.trim() === '*') return '*'
|
||||
if (origin.trim().startsWith('*.')) {
|
||||
const base = origin.trim().slice(2).replace(/\.+$/, '')
|
||||
return isWellFormedDomain(base) ? base : null
|
||||
@@ -126,6 +127,7 @@ function originHostname(origin) {
|
||||
}
|
||||
|
||||
function isWithinDomain(origin, rpId) {
|
||||
if (origin.trim() === '*') return true
|
||||
const hostname = originHostname(origin)
|
||||
if (!hostname) return false
|
||||
return hostname === rpId || hostname.endsWith('.' + rpId)
|
||||
@@ -167,7 +169,7 @@ function validateOrigin(i) {
|
||||
d.originValidation[i] = 'invalid'
|
||||
return
|
||||
}
|
||||
if (value.trim().startsWith('*.')) {
|
||||
if (value.trim() === '*' || value.trim().startsWith('*.')) {
|
||||
d.originValidation[i] = null // wildcards have no concrete site to probe
|
||||
return
|
||||
}
|
||||
@@ -203,15 +205,15 @@ async function testWellKnown() {
|
||||
}
|
||||
watch(() => relatedEntries.value.map(asHttpsOrigin).join('|'), testWellKnown, { immediate: true })
|
||||
|
||||
// Seed the default wildcard entry for a new domain once its rp-id is known,
|
||||
// so the list always shows what is allowed ('*.example.com' = the domain and
|
||||
// all its subdomains). Removing the last in-domain entry is blocked in the
|
||||
// row menu, so the list never becomes empty afterwards.
|
||||
// Seed the default '*' entry for a new domain once its rp-id is known,
|
||||
// so the list always shows what is allowed ('*' = the domain and all its
|
||||
// subdomains, any scheme/port). Removing the last in-domain entry is
|
||||
// blocked in the row menu, so the list never becomes empty afterwards.
|
||||
watch(dialogRpId, rp => {
|
||||
const d = props.dialog?.data
|
||||
if (props.dialog?.type !== 'domain-edit' || !d?.isNew) return
|
||||
if (!d.origins.length && isWellFormedDomain(rp)) {
|
||||
d.origins.push('*.' + rp)
|
||||
d.origins.push('*')
|
||||
d.originValidation.push(null)
|
||||
}
|
||||
})
|
||||
@@ -236,9 +238,10 @@ function setAuthHost(i) {
|
||||
const d = props.dialog?.data
|
||||
if (!d) return
|
||||
let key = entryKey(d.origins[i])
|
||||
if (key.startsWith('*.')) {
|
||||
if (key === '*' || key.startsWith('*.')) {
|
||||
// A wildcard cannot be the auth host — create a concrete auth.<base> entry
|
||||
key = 'auth.' + key.slice(2)
|
||||
const base = key === '*' ? dialogRpId.value : key.slice(2)
|
||||
key = 'auth.' + base
|
||||
if (!d.origins.some(o => entryKey(o) === key)) {
|
||||
d.origins.push(key)
|
||||
d.originValidation.push(null)
|
||||
@@ -387,7 +390,7 @@ function onRemoveOrigin(i) {
|
||||
<p v-else-if="dialog.data.originValidation.some(v => v === 'mismatch')" class="small muted">Some sites are reachable but do not serve this domain.</p>
|
||||
</div>
|
||||
<p class="small muted">
|
||||
Only the listed sites may sign in with this domain's passkeys — <strong>*.{{ dialog.data.rp_id }}</strong> means the domain and all its subdomains.
|
||||
Only the listed sites may sign in with this domain's passkeys — <strong>*</strong> means the domain and all its subdomains on any scheme and port; <strong>*.{{ dialog.data.rp_id }}</strong> restricts that to https.
|
||||
Entries on other domain names become related origins (WebAuthn ROR). The 🔑 site hosts the account and admin interface (set via ⋮).
|
||||
</p>
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ export const hostIP = ip => {
|
||||
// Display-time ordering of a domain's configured origins (the stored
|
||||
// objects are unordered): the auth host first (flagged), then in-domain
|
||||
// entries (exact rp-id, then alphabetical), then related domains
|
||||
// alphabetically. An empty origins object shows as the '*.rp_id' default.
|
||||
// alphabetically. An empty origins object shows as the '*' default
|
||||
// (anything within the rp-id domain, any scheme/port).
|
||||
export function originDisplayEntries(domain) {
|
||||
const origins = domain.origins || {}
|
||||
const keys = Object.keys(origins)
|
||||
@@ -58,7 +59,7 @@ export function originDisplayEntries(domain) {
|
||||
const rows = []
|
||||
if (authKey) rows.push({ key: authKey, auth: true })
|
||||
for (const k of inDomain) rows.push({ key: k, auth: false })
|
||||
if (!keys.length) rows.push({ key: '*.' + domain.rp_id, auth: false })
|
||||
if (!keys.length) rows.push({ key: '*', auth: false })
|
||||
for (const k of Object.keys(domain.related || {}).sort()) {
|
||||
rows.push({ key: k, auth: false, related: true })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user