From 5c280b024ea15888ced726f18e66ba5c6463ac23 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 3 Mar 2026 01:33:37 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20update=20policy=20access=20control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/policy/createResourcePolicy.ts | 29 +- server/routers/policy/getResourcePolicy.ts | 64 ++- .../policy/setResourcePolicyAccessControl.ts | 4 +- .../policies/resource/[niceId]/page.tsx | 2 +- .../resource-policy/EditPolicyForm.tsx | 421 +++++++++++------- src/providers/ResourcePolicyProvider.tsx | 5 +- 6 files changed, 335 insertions(+), 190 deletions(-) diff --git a/server/private/routers/policy/createResourcePolicy.ts b/server/private/routers/policy/createResourcePolicy.ts index dc6780616..29bccd48b 100644 --- a/server/private/routers/policy/createResourcePolicy.ts +++ b/server/private/routers/policy/createResourcePolicy.ts @@ -6,6 +6,8 @@ import createHttpError from "http-errors"; import { fromError } from "zod-validation-error"; import { db, + idp, + idpOrg, orgs, resourcePolicies, rolePolicies, @@ -107,15 +109,23 @@ export async function createResourcePolicy( const { name, sso, userIds, roleIds, skipToIdpId } = parsedBody.data; - const isAuthEnabeld = sso; // other conditions will follow + // Check if Identity provider in `skipToIdpId` exists + if (skipToIdpId) { + const [provider] = await db + .select() + .from(idp) + .innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId)) + .where(and(eq(idp.idpId, skipToIdpId), eq(idpOrg.orgId, orgId))) + .limit(1); - if (!isAuthEnabeld) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "At least one authentication policy must be set: platform SSO, an authentication method, one-time password, or a rule." - ) - ); + if (!provider) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Identity provider not found in this organization" + ) + ); + } } const adminRole = await db @@ -163,7 +173,8 @@ export async function createResourcePolicy( niceId, orgId, name, - sso + sso, + idpId: skipToIdpId }) .returning(); diff --git a/server/routers/policy/getResourcePolicy.ts b/server/routers/policy/getResourcePolicy.ts index 0d33a222e..3b42ffc2e 100644 --- a/server/routers/policy/getResourcePolicy.ts +++ b/server/routers/policy/getResourcePolicy.ts @@ -1,9 +1,17 @@ -import { db, resourcePolicies, rolePolicies, userPolicies } from "@server/db"; +import { + db, + idp, + resourcePolicies, + rolePolicies, + roles, + userPolicies, + users +} from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; import HttpCode from "@server/types/HttpCode"; -import { and, eq, type SQL } from "drizzle-orm"; +import { and, eq, isNull, not, or, type SQL } from "drizzle-orm"; import type { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import z from "zod"; @@ -34,26 +42,48 @@ async function query(params: z.infer) { } const [res] = await db - .select({ - policy: resourcePolicies, - userPolicies, - rolePolicies - }) + .select() .from(resourcePolicies) - .leftJoin( - userPolicies, - eq(userPolicies.resourcePolicyId, resourcePolicies.resourcePolicyId) - ) - .leftJoin( - rolePolicies, - eq(rolePolicies.resourcePolicyId, resourcePolicies.resourcePolicyId) - ) .where(and(...conditions)) .limit(1); - return res; + + if (!res) return null; + + const policyUsers = await db + .select({ + userId: userPolicies.userId, + email: users.email, + name: users.name, + username: users.username, + type: users.type, + idpName: idp.name + }) + .from(userPolicies) + .innerJoin(users, eq(userPolicies.userId, users.userId)) + .leftJoin(idp, eq(idp.idpId, users.idpId)) + .where(eq(userPolicies.resourcePolicyId, res.resourcePolicyId)); + + const policyRoles = await db + .select({ + roleId: rolePolicies.roleId, + name: roles.name + }) + .from(rolePolicies) + .innerJoin( + roles, + and( + eq(rolePolicies.roleId, roles.roleId), + or(isNull(roles.isAdmin), not(roles.isAdmin)) + ) + ) + .where(eq(rolePolicies.resourcePolicyId, res.resourcePolicyId)); + + return { ...res, roles: policyRoles, users: policyUsers }; } -export type GetResourcePolicyResponse = Awaited>; +export type GetResourcePolicyResponse = NonNullable< + Awaited> +>; registry.registerPath({ method: "get", diff --git a/server/routers/policy/setResourcePolicyAccessControl.ts b/server/routers/policy/setResourcePolicyAccessControl.ts index 926478db8..7bbbe8a38 100644 --- a/server/routers/policy/setResourcePolicyAccessControl.ts +++ b/server/routers/policy/setResourcePolicyAccessControl.ts @@ -16,14 +16,14 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { and, eq, inArray, ne, type InferInsertModel } from "drizzle-orm"; +import { and, eq, inArray, ne } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const setResourcePolicyAcccessControlBodySchema = z.strictObject({ sso: z.boolean(), userIds: z.array(z.string()), roleIds: z.array(z.int().positive()), - skipToIdpId: z.int().positive().optional() + skipToIdpId: z.int().positive().optional().nullish() }); const setResourcePolicyAccessControlParamsSchema = z.strictObject({ diff --git a/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx b/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx index 9c18c9b96..5519506b9 100644 --- a/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx +++ b/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx @@ -40,7 +40,7 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index 88c6ceafa..20c7a76e6 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -123,6 +123,7 @@ import { useCallback, useMemo, useState, useActionState } from "react"; import { UseFormReturn, useForm, useWatch } from "react-hook-form"; import router from "next/navigation"; import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; +import { t } from "@faker-js/faker/dist/airline-CWrCIUHH"; // ─── EditPolicyForm ───────────────────────────────────────────────────────── @@ -426,6 +427,10 @@ export function PolicyUsersRolesSection({ const { policy } = useResourcePolicyContext(); + const api = createApiClient(useEnvContext()); + + console.log({ policy }); + const form = useForm({ resolver: zodResolver( createPolicySchema.pick({ @@ -437,7 +442,15 @@ export function PolicyUsersRolesSection({ ), defaultValues: { sso: policy.sso, - skipToIdpId: policy.idpId + skipToIdpId: policy.idpId, + roles: policy.roles.map((role) => ({ + id: role.roleId.toString(), + text: role.name + })), + users: policy.users.map((user) => ({ + id: user.userId, + text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + })) } }); @@ -453,167 +466,257 @@ export function PolicyUsersRolesSection({ number | null >(null); + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + + async function onSubmit() { + const isValid = await form.trigger(); + + if (!isValid) return; + + const payload = form.getValues(); + + try { + const res = await api + .put>( + `/resource-policy/${policy.resourcePolicyId}/access-control`, + { + sso: payload.sso, + userIds: payload.users.map((user) => user.id), + roleIds: payload.roles.map((role) => Number(role.id)), + skipToIdpId: payload.skipToIdpId + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: t("success"), + description: t("policyUpdatedSuccess") + }); + } + } catch (e) { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: t("policyErrorUpdateMessageDescription") + }); + } + } + return ( - - - - {t("resourceUsersRoles")} - - - {t("resourcePolicyUsersRolesDescription")} - - - - - { - console.log(`form.setValue("sso", ${val})`); - form.setValue("sso", val); - }} - /> - - {ssoEnabled && ( - <> - ( - - {t("roles")} - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={true} - autocompleteOptions={allRoles} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t("resourceRoleDescription")} - - - )} - /> - ( - - {t("users")} - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={true} - autocompleteOptions={allUsers} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - - )} - - {ssoEnabled && allIdps.length > 0 && ( -
- - -

- {t("defaultIdentityProviderDescription")} -

-
- )} -
-
-
+ ( + + + {t("users")} + + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers + } + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} + + {ssoEnabled && allIdps.length > 0 && ( +
+ + +

+ {t( + "defaultIdentityProviderDescription" + )} +

+
+ )} + + + +
+ +
+ + + ); } diff --git a/src/providers/ResourcePolicyProvider.tsx b/src/providers/ResourcePolicyProvider.tsx index c6300304b..e80704dc4 100644 --- a/src/providers/ResourcePolicyProvider.tsx +++ b/src/providers/ResourcePolicyProvider.tsx @@ -38,13 +38,14 @@ export function ResourcePolicyProvider({ }; return ( - + {children} ); } -export type ResourcePolicyContextType = GetResourcePolicyResponse & { +export type ResourcePolicyContextType = { + policy: GetResourcePolicyResponse; updatePolicy: (updatedPolicy: Partial) => void; };