- Added new OIDC configuration options in `.env.example`. - Expanded documentation in `AGENTS.md` to include OIDC endpoints and admin UI. - Updated ReUI skill version and component count from 17 to 20 across various documentation files. - Enhanced `README.md` and other related files to reflect the new component structure and usage guidelines. Co-authored-by: Cursor <[email protected]>
102 lines
2.8 KiB
TypeScript
102 lines
2.8 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
export const OIDC_SCOPES = ['openid', 'profile', 'email', 'groups'] as const
|
|
export type OidcScope = (typeof OIDC_SCOPES)[number]
|
|
|
|
/** Remote groups for Technitium Group Map (and similar RPs). */
|
|
export const TECHNITIUM_OIDC_GROUPS = {
|
|
admins: 'technitium_admins',
|
|
dnsAdmins: 'technitium_dns_admins',
|
|
dhcpAdmins: 'technitium_dhcp_admins',
|
|
} as const
|
|
|
|
/**
|
|
* Map portal permissions / admin flag → OIDC group names for Technitium.
|
|
* Used in id_token + userinfo `groups` and `roles` claims.
|
|
*/
|
|
export function oidcGroupsForUser(input: {
|
|
isAdmin: boolean
|
|
permissions: readonly string[]
|
|
}): string[] {
|
|
const granted = new Set(input.permissions)
|
|
const groups: string[] = []
|
|
if (input.isAdmin || granted.has('dns:console:admin')) {
|
|
groups.push(TECHNITIUM_OIDC_GROUPS.admins)
|
|
}
|
|
if (
|
|
granted.has('dns:dns:write') ||
|
|
granted.has('dns:dns:admin') ||
|
|
granted.has('dns:console:admin') ||
|
|
input.isAdmin
|
|
) {
|
|
groups.push(TECHNITIUM_OIDC_GROUPS.dnsAdmins)
|
|
}
|
|
if (
|
|
granted.has('dns:dhcp:write') ||
|
|
granted.has('dns:dhcp:admin') ||
|
|
granted.has('dns:console:admin') ||
|
|
input.isAdmin
|
|
) {
|
|
groups.push(TECHNITIUM_OIDC_GROUPS.dhcpAdmins)
|
|
}
|
|
return [...new Set(groups)]
|
|
}
|
|
|
|
export const createOidcClientRequestSchema = z.object({
|
|
name: z.string().min(1).max(200),
|
|
redirect_uris: z.array(z.string().url()).min(1),
|
|
scopes: z
|
|
.array(z.enum(OIDC_SCOPES))
|
|
.min(1)
|
|
.default([...OIDC_SCOPES]),
|
|
enabled: z.boolean().default(true),
|
|
})
|
|
export type CreateOidcClientRequest = z.infer<
|
|
typeof createOidcClientRequestSchema
|
|
>
|
|
|
|
export const patchOidcClientRequestSchema = z.object({
|
|
name: z.string().min(1).max(200).optional(),
|
|
redirect_uris: z.array(z.string().url()).min(1).optional(),
|
|
scopes: z.array(z.enum(OIDC_SCOPES)).min(1).optional(),
|
|
enabled: z.boolean().optional(),
|
|
})
|
|
export type PatchOidcClientRequest = z.infer<
|
|
typeof patchOidcClientRequestSchema
|
|
>
|
|
|
|
export const oidcClientPublicSchema = z.object({
|
|
id: z.string(),
|
|
client_id: z.string(),
|
|
name: z.string(),
|
|
redirect_uris: z.array(z.string()),
|
|
scopes: z.array(z.string()),
|
|
enabled: z.boolean(),
|
|
created_at: z.string(),
|
|
updated_at: z.string(),
|
|
})
|
|
export type OidcClientPublic = z.infer<typeof oidcClientPublicSchema>
|
|
|
|
export const oidcClientCreatedSchema = oidcClientPublicSchema.extend({
|
|
client_secret: z.string(),
|
|
})
|
|
export type OidcClientCreated = z.infer<typeof oidcClientCreatedSchema>
|
|
|
|
/** True when return_to is portal OIDC authorize (no JWT fragment handoff). */
|
|
export function isPortalOidcAuthorizeUrl(
|
|
returnTo: string,
|
|
issuer: string,
|
|
): boolean {
|
|
try {
|
|
const url = new URL(returnTo)
|
|
const iss = new URL(issuer)
|
|
if (url.origin !== iss.origin) return false
|
|
return (
|
|
url.pathname === '/oauth/authorize' ||
|
|
url.pathname === '/oauth/authorize/'
|
|
)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|