diff --git a/external-ui/src/auth/session-bootstrap.tsx b/external-ui/src/auth/session-bootstrap.tsx index 1043ab6e..c2874464 100644 --- a/external-ui/src/auth/session-bootstrap.tsx +++ b/external-ui/src/auth/session-bootstrap.tsx @@ -23,6 +23,7 @@ function toAuthenticatedSession(response: AuthSessionResponse): AuthenticatedSes n8nUser: response.n8nUser, permissions: response.permissions, tenantRoles: response.tenantRoles ?? [], + tenantGroups: response.tenantGroups ?? [], }; } diff --git a/external-ui/src/components/action-handlers/show-form-handler.tsx b/external-ui/src/components/action-handlers/show-form-handler.tsx index ba31b06c..9a82144d 100644 --- a/external-ui/src/components/action-handlers/show-form-handler.tsx +++ b/external-ui/src/components/action-handlers/show-form-handler.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { WilActionItem } from '../../services/backend/wil'; import { postWilChefsToken, postWilCallback } from '../../services/backend/wil'; import { getStoredAppToken } from '../../services/backend/axios'; -import { useSessionSnapshot } from '../../state/session'; +import { useSessionSnapshot, useTenantGroupsById, useTenantRolesById } from '../../state/session'; import { ChefsFormPanel } from '../chefs/chefs-form-panel'; import type { ChefsFormPanelInitData } from '../chefs/chefs-form-panel'; import { buildTokenObject, buildUserObject, buildUserProfile } from '../chefs/user-claims-utils'; @@ -23,6 +23,8 @@ async function initializeForm(params: { actionId: string; payload: Record; claims: Record; + tenantRoles: readonly string[]; + tenantGroups: readonly string[]; }): Promise { const tokenResponse = await postWilChefsToken({ tenantId: params.tenantId, actionId: params.actionId }); const formPreFillData = (params.payload.formPreFillData as Record) ?? {}; @@ -36,7 +38,7 @@ async function initializeForm(params: { submissionId, prefillData: { ...formPreFillData, ...buildUserProfile(params.claims) }, token: buildTokenObject(params.claims), - user: buildUserObject(params.claims), + user: buildUserObject(params.claims, { roles: params.tenantRoles, groups: params.tenantGroups }), headers: userToken ? { Authorization: `Bearer ${userToken}` } : {}, }; } @@ -44,6 +46,8 @@ async function initializeForm(params: { /** Renders a CHEFS form for a `showform` action and posts the submission to the WIL callback API. */ export function ShowFormHandler({ action, tenantId, onInteractionSuccess, onRefresh }: Readonly) { const { session } = useSessionSnapshot(); + const tenantRoles = useTenantRolesById(tenantId); + const tenantGroups = useTenantGroupsById(tenantId); const queryClient = useQueryClient(); const onInteractionSuccessRef = useRef(onInteractionSuccess); const { claimError, setClaimError } = useClaimVerification({ @@ -66,11 +70,18 @@ export function ShowFormHandler({ action, tenantId, onInteractionSuccess, onRefr // Re-initialize whenever the action or session claims change useEffect(() => { if (!session?.oidc.claims) return; - initMutation.mutate({ tenantId, actionId: action.id, payload: action.payload, claims: session.oidc.claims }); + initMutation.mutate({ + tenantId, + actionId: action.id, + payload: action.payload, + claims: session.oidc.claims, + tenantRoles, + tenantGroups, + }); callbackMutation.reset(); setClaimError(null); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [action.id, action.payload, session?.oidc.claims, tenantId]); + }, [action.id, action.payload, session?.oidc.claims, tenantId, tenantRoles, tenantGroups]); function handleRefresh() { queryClient.invalidateQueries({ queryKey: ['wil-actions'] }); diff --git a/external-ui/src/components/chefs/user-claims-utils.ts b/external-ui/src/components/chefs/user-claims-utils.ts index 37c928cc..2d1514fa 100644 --- a/external-ui/src/components/chefs/user-claims-utils.ts +++ b/external-ui/src/components/chefs/user-claims-utils.ts @@ -12,7 +12,10 @@ export function buildTokenObject(claims: Record): Record): Record { +export function buildUserObject( + claims: Record, + tenant?: { roles: readonly string[]; groups: readonly string[] }, +): Record { return { name: claims.display_name, firstName: claims.given_name, @@ -20,6 +23,8 @@ export function buildUserObject(claims: Record): Record; + tenantRoles: readonly string[]; + tenantGroups: readonly string[]; }): Promise { const tokenResponse = await getTriggerChefsToken({ tenantId: params.tenantId, triggerId: params.triggerId }); const userToken = getStoredAppToken(); @@ -21,7 +23,7 @@ async function initializeForm(params: { formId: tokenResponse.formId, baseUrl: tokenResponse.baseUrl, token: buildTokenObject(params.claims), - user: buildUserObject(params.claims), + user: buildUserObject(params.claims, { roles: params.tenantRoles, groups: params.tenantGroups }), headers: userToken ? { Authorization: `Bearer ${userToken}` } : {}, }; } @@ -33,6 +35,8 @@ interface TriggerChefsPreviewProps { export function TriggerChefsPreview({ trigger, tenantId }: Readonly) { const { session } = useSessionSnapshot(); + const tenantRoles = useTenantRolesById(tenantId); + const tenantGroups = useTenantGroupsById(tenantId); const initMutation = useMutation({ mutationFn: initializeForm }); @@ -54,13 +58,19 @@ export function TriggerChefsPreview({ trigger, tenantId }: Readonly { if (!session?.oidc.claims) return; - initMutation.mutate({ tenantId, triggerId: trigger.id, claims: session.oidc.claims }); + initMutation.mutate({ + tenantId, + triggerId: trigger.id, + claims: session.oidc.claims, + tenantRoles, + tenantGroups, + }); // Reset submission state when trigger or session changes setSubmitStatus('idle'); setSubmitError(null); callbackMutation.reset(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [trigger.id, tenantId, session?.oidc.claims]); + }, [trigger.id, tenantId, session?.oidc.claims, tenantRoles, tenantGroups]); // callbackMutation.mutate is stable across renders (TanStack Query wraps it in useCallback // with a ref internally), so it is safe as a useCallback dep without causing churn. diff --git a/external-ui/src/services/backend/auth.ts b/external-ui/src/services/backend/auth.ts index 77fbd2ca..4a2e6a86 100644 --- a/external-ui/src/services/backend/auth.ts +++ b/external-ui/src/services/backend/auth.ts @@ -35,6 +35,7 @@ export type AuthSessionResponse = { n8nUser: AuthSessionN8nUser | null; permissions: Permissions | null; tenantRoles: TenantRole[] | null; + tenantGroups: TenantGroup[] | null; }; export type AuthenticatedSession = { @@ -43,6 +44,7 @@ export type AuthenticatedSession = { n8nUser: AuthSessionN8nUser; permissions: Permissions; tenantRoles: TenantRole[]; + tenantGroups: TenantGroup[]; }; export type Permissions = { @@ -63,6 +65,12 @@ export type TenantRole = { roles: readonly string[]; }; +export type TenantGroup = { + tenantId: string; + tenantName: string; + groups: readonly string[]; +}; + export type WhoamiResponse = { userAgent: string | null; oidc: { diff --git a/external-ui/src/state/session.ts b/external-ui/src/state/session.ts index 6d0c1c28..b020c8a0 100644 --- a/external-ui/src/state/session.ts +++ b/external-ui/src/state/session.ts @@ -1,5 +1,5 @@ import { proxy, useSnapshot } from 'valtio'; -import type { AuthenticatedSession, TenantRole } from '../services/backend/auth'; +import type { AuthenticatedSession, TenantGroup, TenantRole } from '../services/backend/auth'; type SessionState = { session: AuthenticatedSession | null; @@ -65,6 +65,22 @@ export function useTenantRolesById(tenantId: string): readonly string[] { return useTenantRoles().find((t) => t.tenantId === tenantId)?.roles ?? []; } +/** + * Hook to get the current authenticated user's tenants with groups. + * Returns an empty array if the user is not logged in or has no tenant groups. + */ +export function useTenantGroups(): readonly TenantGroup[] { + return useSnapshot(sessionState).session?.tenantGroups ?? []; +} + +/** + * Hook to get the current authenticated user's groups for a specific tenant. + * Returns an empty array if the user is not logged in or has no groups for the specified tenant. + */ +export function useTenantGroupsById(tenantId: string): readonly string[] { + return useTenantGroups().find((t) => t.tenantId === tenantId)?.groups ?? []; +} + /** * Hook to check if the current authenticated user has a specific role in any tenant. * Returns false if the user is not logged in or does not have the specified role. diff --git a/helm/main/values-c89a45-dev-gold.yaml b/helm/main/values-c89a45-dev-gold.yaml index 72f13cae..bada7cd4 100644 --- a/helm/main/values-c89a45-dev-gold.yaml +++ b/helm/main/values-c89a45-dev-gold.yaml @@ -15,7 +15,7 @@ n8n: UI_OIDC_REDIS_PREFIX: "chwf:ui-oidc:dev:" CHEFS_BASE_URL: https://chefs-dev.apps.silver.devops.gov.bc.ca FEATURES_ENABLED: "wil, project, workflow-share, tenant-project-sync" - CSTAR_BASE_URL: https://dev-workflow-mock.apps.gold.devops.gov.bc.ca + CSTAR_BASE_URL: https://dev.connect.digital.gov.bc.ca route: enabled: true diff --git a/helm/main/values-c89a45-dev-golddr.yaml b/helm/main/values-c89a45-dev-golddr.yaml index 9d287044..4f300adf 100644 --- a/helm/main/values-c89a45-dev-golddr.yaml +++ b/helm/main/values-c89a45-dev-golddr.yaml @@ -14,7 +14,7 @@ n8n: CHEFS_GATEWAY_URL: https://chefs-dev.apps.silver.devops.gov.bc.ca/app/gateway/v1 CHEFS_BASE_URL: https://chefs-dev.apps.silver.devops.gov.bc.ca FEATURES_ENABLED: "wil, project, workflow-share, tenant-project-sync" - CSTAR_BASE_URL: https://dev-workflow-mock.apps.gold.devops.gov.bc.ca + CSTAR_BASE_URL: https://dev.connect.digital.gov.bc.ca extraExcludedNodes: - "n8n-nodes-base.scheduleTrigger"