import { AuthAction, Resource, Role } from '~/composables/gql/graphql.js'; export interface RawPermission { resource: string; actions: AuthAction[]; } export interface AuthorizationLinkParams { appName: string; appDescription?: string; roles?: Role[]; rawPermissions?: RawPermission[]; redirectUrl?: string; state?: string; } export interface AuthorizationFormData { name: string; description: string; roles?: Role[]; customPermissions?: Array<{ resources: Resource[]; actions: AuthAction[]; }>; } /** * Convert AuthAction enum to simple action verb * E.g., CREATE_ANY -> create, READ_OWN -> read * @deprecated Use extractActionWithPossession for possession-aware extraction */ export function extractActionVerb(action: AuthAction | string): string { const actionStr = String(action); // Handle enum values like CREATE_ANY, READ_ANY if (actionStr.includes('_')) { return actionStr.split('_')[0].toLowerCase(); } // Handle scope format like 'read:any' if (actionStr.includes(':')) { return actionStr.split(':')[0].toLowerCase(); } // Already just a verb return actionStr.toLowerCase(); } /** * Convert AuthAction enum to lowercase format with possession * E.g., CREATE_ANY -> create_any, READ_OWN -> read_own */ export function extractActionWithPossession(action: AuthAction | string): string { // Just convert the full enum value to lowercase return String(action).toLowerCase(); } /** * Encode permissions into efficient scope strings * Groups resources with identical action sets using a compact format: * - Individual: "docker:read_any", "vms:update_own" * - Grouped: "docker+vms:read_any+update_own" (resources sharing same actions) */ export function encodePermissionsToScopes(roles: Role[] = [], rawPermissions: RawPermission[] = []): string[] { const scopes: string[] = []; // Add role scopes for (const role of roles) { scopes.push(`role:${role.toLowerCase()}`); } // Skip empty permissions const validPermissions = rawPermissions.filter(perm => perm.actions && perm.actions.length > 0); // First, merge all permissions for the same resource const resourceActionsMap = new Map>(); for (const perm of validPermissions) { const resourceName = perm.resource.toLowerCase(); if (!resourceActionsMap.has(resourceName)) { resourceActionsMap.set(resourceName, new Set()); } const actionsSet = resourceActionsMap.get(resourceName)!; for (const action of perm.actions) { actionsSet.add(extractActionWithPossession(action)); } } // Now group resources by their action sets for efficient encoding const actionGroups = new Map>(); for (const [resourceName, actionsSet] of resourceActionsMap) { const actionsWithPossession = Array.from(actionsSet).sort(); // Create a key from the sorted actions const actionKey = actionsWithPossession.join('+'); if (!actionGroups.has(actionKey)) { actionGroups.set(actionKey, new Set()); } actionGroups.get(actionKey)!.add(resourceName); } // Generate efficient scopes for (const [actions, resourcesSet] of actionGroups.entries()) { // Convert Set to sorted array for consistent output const resources = Array.from(resourcesSet).sort(); if (resources.length === 1) { // Single resource: "docker:read_any+update_own" scopes.push(`${resources[0]}:${actions}`); } else { // Multiple resources with same actions: "docker+vms:read_any+update_own" scopes.push(`${resources.join('+')}:${actions}`); } } return scopes; } /** * Decode scope strings back to permissions and roles * Supports both individual and grouped formats: * - "role:admin" -> role * - "docker:read_any" -> single permission with possession * - "docker+vms:read_any+update_own" -> multiple permissions with same actions * - "docker:*" -> wildcard (all CRUD actions) */ export function decodeScopesToPermissions(scopes: string[]): { permissions: Array<{ resource: Resource; actions: AuthAction[] }>, roles: Role[] } { const roles: Role[] = []; // Use a map to merge permissions for the same resource const resourcePermissions = new Map>(); for (const scope of scopes) { if (!scope) continue; if (scope.startsWith('role:')) { // Handle role scope const roleStr = scope.substring(5).toUpperCase(); if (Object.values(Role).includes(roleStr as Role)) { roles.push(roleStr as Role); } } else { // Handle permission scope (potentially grouped) const [resourcesPart, actionsPart] = scope.split(':'); if (!resourcesPart || !actionsPart) continue; // Split grouped resources (docker+vms) const resourceNames = resourcesPart.split('+'); // Parse actions let actions: AuthAction[]; if (actionsPart === '*') { // Wildcard: all CRUD actions actions = [ AuthAction.CREATE_ANY, AuthAction.READ_ANY, AuthAction.UPDATE_ANY, AuthAction.DELETE_ANY ]; } else { // Split grouped actions (read_any+update_own) const actionParts = actionsPart.split('+'); actions = actionParts .map(actionStr => { // Convert to AuthAction enum (e.g., "read_any" -> "READ_ANY") const enumValue = actionStr.toUpperCase() as AuthAction; return Object.values(AuthAction).includes(enumValue) ? enumValue : null; }) .filter((action): action is AuthAction => action !== null); } // Add actions to each resource for (const resourceName of resourceNames) { const resourceUpper = resourceName.toUpperCase(); const resource = Object.values(Resource).find(r => r === resourceUpper) as Resource; if (resource && actions.length > 0) { if (!resourcePermissions.has(resource)) { resourcePermissions.set(resource, new Set()); } actions.forEach(action => resourcePermissions.get(resource)!.add(action)); } } } } // Convert map to array of permissions const permissions = Array.from(resourcePermissions.entries()).map(([resource, actionsSet]) => ({ resource, actions: Array.from(actionsSet) })); return { permissions, roles }; } /** * Convert scopes to form data structure with grouped permissions */ export function scopesToFormData(scopes: string[], name: string, description: string = ''): AuthorizationFormData { const { permissions, roles } = decodeScopesToPermissions(scopes); // Group permissions by their action sets for the form const permissionGroups = new Map; actions: Set }>(); for (const perm of permissions) { // Create a key based on sorted actions to group resources const actionKey = [...perm.actions].sort().join(','); if (!permissionGroups.has(actionKey)) { permissionGroups.set(actionKey, { resources: new Set(), actions: new Set(perm.actions), }); } permissionGroups.get(actionKey)!.resources.add(perm.resource); } // Convert to array format expected by form const customPermissions = Array.from(permissionGroups.values()).map(group => ({ resources: Array.from(group.resources), actions: Array.from(group.actions), })); return { name, description, roles, customPermissions: customPermissions.length > 0 ? customPermissions : [], }; } /** * Generate authorization URL from params */ export function generateAuthorizationUrl(params: AuthorizationLinkParams): string { const { appName, appDescription, roles = [], rawPermissions = [], redirectUrl, state } = params; // Compute redirectUrl with SSR safety const computedRedirectUrl = redirectUrl || ( typeof window !== 'undefined' ? window.location.origin + '/api-key-created' : '/api-key-created' ); const scopes = encodePermissionsToScopes(roles, rawPermissions); // Build URL parameters const urlParams = new URLSearchParams({ name: appName, redirect_uri: computedRedirectUrl, scopes: scopes.join(','), }); if (appDescription) { urlParams.set('description', appDescription); } if (state) { urlParams.set('state', state); } // Use current origin for the authorization URL with SSR safety const baseUrl = typeof window !== 'undefined' ? `${window.location.origin}/Tools/ApiKeyAuthorize` : '/Tools/ApiKeyAuthorize'; return `${baseUrl}?${urlParams.toString()}`; } /** * Build callback URL with API key or error */ export function buildCallbackUrl( redirectUri: string, apiKey?: string, error?: string, state?: string ): string { try { const url = new URL(redirectUri); if (apiKey) { url.searchParams.set('api_key', apiKey); } if (error) { url.searchParams.set('error', error); } if (state) { url.searchParams.set('state', state); } return url.toString(); } catch { throw new Error('Invalid redirect URI'); } } // Alias for backward compatibility export const generateScopes = encodePermissionsToScopes;