update policy access control

This commit is contained in:
Fred KISSIE
2026-03-03 01:33:37 +01:00
parent 033cc62ce7
commit 5c280b024e
6 changed files with 335 additions and 190 deletions

View File

@@ -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();

View File

@@ -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<typeof getResourcePolicySchema>) {
}
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<ReturnType<typeof query>>;
export type GetResourcePolicyResponse = NonNullable<
Awaited<ReturnType<typeof query>>
>;
registry.registerPath({
method: "get",

View File

@@ -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({

View File

@@ -40,7 +40,7 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
<div className="flex justify-between">
<SettingsSectionTitle
title={t("resourcePolicySetting", {
policyName: policyResponse.policy.name
policyName: policyResponse.name
})}
description={t("resourcePolicySettingDescription")}
/>

View File

@@ -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<AxiosResponse<{}>>(
`/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 (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("resourceUsersRoles")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourcePolicyUsersRolesDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<SwitchInput
id="sso-toggle"
label={t("ssoUse")}
defaultChecked={ssoEnabled}
onCheckedChange={(val) => {
console.log(`form.setValue("sso", ${val})`);
form.setValue("sso", val);
}}
/>
{ssoEnabled && (
<>
<FormField
control={form.control}
name="roles"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>{t("roles")}</FormLabel>
<FormControl>
<TagInput
{...field}
activeTagIndex={
activeRolesTagIndex
}
setActiveTagIndex={
setActiveRolesTagIndex
}
placeholder={t(
"accessRoleSelect2"
)}
size="sm"
tags={form.getValues().roles}
setTags={(newRoles) => {
form.setValue(
"roles",
newRoles as [
Tag,
...Tag[]
]
);
}}
enableAutocomplete={true}
autocompleteOptions={allRoles}
allowDuplicates={false}
restrictTagsToAutocompleteOptions={
true
}
sortTags={true}
/>
</FormControl>
<FormMessage />
<FormDescription>
{t("resourceRoleDescription")}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="users"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>{t("users")}</FormLabel>
<FormControl>
<TagInput
{...field}
activeTagIndex={
activeUsersTagIndex
}
setActiveTagIndex={
setActiveUsersTagIndex
}
placeholder={t(
"accessUserSelect"
)}
size="sm"
tags={form.getValues().users}
setTags={(newUsers) => {
form.setValue(
"users",
newUsers as [
Tag,
...Tag[]
]
);
}}
enableAutocomplete={true}
autocompleteOptions={allUsers}
allowDuplicates={false}
restrictTagsToAutocompleteOptions={
true
}
sortTags={true}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{ssoEnabled && allIdps.length > 0 && (
<div className="space-y-2">
<label className="text-sm font-medium">
{t("defaultIdentityProvider")}
</label>
<Select
onValueChange={(value) => {
if (value === "none") {
form.setValue("skipToIdpId", null);
} else {
const id = parseInt(value);
form.setValue("skipToIdpId", id);
}
<Form {...form}>
<form action={formAction}>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("resourceUsersRoles")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourcePolicyUsersRolesDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<SwitchInput
id="sso-toggle"
label={t("ssoUse")}
defaultChecked={ssoEnabled}
onCheckedChange={(val) => {
console.log(`form.setValue("sso", ${val})`);
form.setValue("sso", val);
}}
value={
selectedIdpId
? selectedIdpId.toString()
: "none"
}
>
<SelectTrigger className="w-full mt-1">
<SelectValue
placeholder={t("selectIdpPlaceholder")}
/>
{ssoEnabled && (
<>
<FormField
control={form.control}
name="roles"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>
{t("roles")}
</FormLabel>
<FormControl>
<TagInput
{...field}
activeTagIndex={
activeRolesTagIndex
}
setActiveTagIndex={
setActiveRolesTagIndex
}
placeholder={t(
"accessRoleSelect2"
)}
size="sm"
tags={
form.getValues()
.roles
}
setTags={(newRoles) => {
form.setValue(
"roles",
newRoles as [
Tag,
...Tag[]
]
);
}}
enableAutocomplete={
true
}
autocompleteOptions={
allRoles
}
allowDuplicates={false}
restrictTagsToAutocompleteOptions={
true
}
sortTags={true}
/>
</FormControl>
<FormMessage />
<FormDescription>
{t(
"resourceRoleDescription"
)}
</FormDescription>
</FormItem>
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
{t("none")}
</SelectItem>
{allIdps.map((idp) => (
<SelectItem
key={idp.id}
value={idp.id.toString()}
>
{idp.text}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{t("defaultIdentityProviderDescription")}
</p>
</div>
)}
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
<FormField
control={form.control}
name="users"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>
{t("users")}
</FormLabel>
<FormControl>
<TagInput
{...field}
activeTagIndex={
activeUsersTagIndex
}
setActiveTagIndex={
setActiveUsersTagIndex
}
placeholder={t(
"accessUserSelect"
)}
size="sm"
tags={
form.getValues()
.users
}
setTags={(newUsers) => {
form.setValue(
"users",
newUsers as [
Tag,
...Tag[]
]
);
}}
enableAutocomplete={
true
}
autocompleteOptions={
allUsers
}
allowDuplicates={false}
restrictTagsToAutocompleteOptions={
true
}
sortTags={true}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{ssoEnabled && allIdps.length > 0 && (
<div className="space-y-2">
<label className="text-sm font-medium">
{t("defaultIdentityProvider")}
</label>
<Select
onValueChange={(value) => {
if (value === "none") {
form.setValue(
"skipToIdpId",
null
);
} else {
const id = parseInt(value);
form.setValue(
"skipToIdpId",
id
);
}
}}
value={
selectedIdpId
? selectedIdpId.toString()
: "none"
}
>
<SelectTrigger className="w-full mt-1">
<SelectValue
placeholder={t(
"selectIdpPlaceholder"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
{t("none")}
</SelectItem>
{allIdps.map((idp) => (
<SelectItem
key={idp.id}
value={idp.id.toString()}
>
{idp.text}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{t(
"defaultIdentityProviderDescription"
)}
</p>
</div>
)}
</SettingsSectionForm>
</SettingsSectionBody>
<div className="flex py-6 justify-end">
<Button
type="submit"
loading={isSubmitting}
disabled={isSubmitting}
>
{t("saveSettings")}
</Button>
</div>
</SettingsSection>
</form>
</Form>
);
}

View File

@@ -38,13 +38,14 @@ export function ResourcePolicyProvider({
};
return (
<ResourcePolicyContext value={{ ...policy, updatePolicy }}>
<ResourcePolicyContext value={{ policy, updatePolicy }}>
{children}
</ResourcePolicyContext>
);
}
export type ResourcePolicyContextType = GetResourcePolicyResponse & {
export type ResourcePolicyContextType = {
policy: GetResourcePolicyResponse;
updatePolicy: (updatedPolicy: Partial<GetResourcePolicyResponse>) => void;
};