create policy endpoitn

This commit is contained in:
Fred KISSIE
2026-02-24 06:31:43 +01:00
parent 335411de4c
commit 1d709b551a
12 changed files with 324 additions and 43 deletions

View File

@@ -637,6 +637,10 @@
"rulesNoOne": "No rules. Add a rule using the form.", "rulesNoOne": "No rules. Add a rule using the form.",
"rulesOrder": "Rules are evaluated by priority in ascending order.", "rulesOrder": "Rules are evaluated by priority in ascending order.",
"rulesSubmit": "Save Rules", "rulesSubmit": "Save Rules",
"policyErrorCreate": "Error creating policy",
"policyErrorCreateDescription": "An error occurred when creating the policy",
"policyErrorCreateMessageDescription": "An unexpected error occurred",
"policyCreatedSuccess": "Resource policy succesfully created",
"resourceErrorCreate": "Error creating resource", "resourceErrorCreate": "Error creating resource",
"resourceErrorCreateDescription": "An error occurred when creating the resource", "resourceErrorCreateDescription": "An error occurred when creating the resource",
"resourceErrorCreateMessage": "Error creating resource:", "resourceErrorCreateMessage": "Error creating resource:",

View File

@@ -133,13 +133,13 @@ export enum ActionsEnum {
listApprovals = "listApprovals", listApprovals = "listApprovals",
updateApprovals = "updateApprovals", updateApprovals = "updateApprovals",
listResourcePolicies = "listResourcePolicies", listResourcePolicies = "listResourcePolicies",
createResourcePolicies = "createResourcePolicies", createResourcePolicy = "createResourcePolicy",
updateResourcePolicies = "updateResourcePolicies", updateResourcePolicy = "updateResourcePolicy",
deleteResourcePolicies = "deleteResourcePolicies", deleteResourcePolicy = "deleteResourcePolicy",
listResourcePolicyRoles = "listResourcePolicyRoles", listResourcePolicyRoles = "listResourcePolicyRoles",
setResourcePolicyRoles = "setResourcePolicyRoles", setResourcePolicyRoles = "setResourcePolicyRoles",
listResourcePolicyUsers = "listResourcePolicyUsers", listResourcePolicyUsers = "listResourcePolicyUsers",
setResourcePolicyUsers = "setResourcePolicyUsers", setResourcePolicyUsers = "setResourcePolicyUsers"
} }
export async function checkUserActionPermission( export async function checkUserActionPermission(

View File

@@ -1,6 +1,12 @@
import { join } from "path"; import { join } from "path";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { clients, db, resources, siteResources } from "@server/db"; import {
clients,
db,
resourcePolicies,
resources,
siteResources
} from "@server/db";
import { randomInt } from "crypto"; import { randomInt } from "crypto";
import { exitNodes, sites } from "@server/db"; import { exitNodes, sites } from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
@@ -107,6 +113,35 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
} }
} }
export async function getUniqueResourcePolicyName(
orgId: string
): Promise<string> {
let loops = 0;
while (true) {
if (loops > 100) {
throw new Error("Could not generate a unique name");
}
const name = generateName();
const policyCount = await db
.select({
niceId: resourcePolicies.niceId,
orgId: resourcePolicies.orgId
})
.from(resourcePolicies)
.where(
and(
eq(resourcePolicies.niceId, name),
eq(resourcePolicies.orgId, orgId)
)
);
if (policyCount.length === 0) {
return name;
}
loops++;
}
}
export async function getUniqueSiteResourceName( export async function getUniqueSiteResourceName(
orgId: string orgId: string
): Promise<string> { ): Promise<string> {

View File

@@ -26,6 +26,7 @@ import * as misc from "#private/routers/misc";
import * as reKey from "#private/routers/re-key"; import * as reKey from "#private/routers/re-key";
import * as approval from "#private/routers/approvals"; import * as approval from "#private/routers/approvals";
import * as resource from "#private/routers/resource"; import * as resource from "#private/routers/resource";
import * as policy from "#private/routers/policy";
import { import {
verifyOrgAccess, verifyOrgAccess,
@@ -349,9 +350,19 @@ authenticated.get(
verifyLimits, verifyLimits,
verifyUserHasAction(ActionsEnum.listResourcePolicies), verifyUserHasAction(ActionsEnum.listResourcePolicies),
logActionAudit(ActionsEnum.listResourcePolicies), logActionAudit(ActionsEnum.listResourcePolicies),
resource.listResourcePolicies policy.listResourcePolicies
); );
authenticated.post(
"/org/:orgId/resource-policy",
verifyValidLicense,
// verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ?
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createResourcePolicy),
logActionAudit(ActionsEnum.createResourcePolicy),
policy.createResourcePolicy
);
authenticated.put( authenticated.put(
"/org/:orgId/approvals/:approvalId", "/org/:orgId/approvals/:approvalId",

View File

@@ -0,0 +1,178 @@
import { Request, Response, NextFunction } from "express";
import z from "zod";
import { OpenAPITags, registry } from "@server/openApi";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { fromError } from "zod-validation-error";
import {
db,
orgs,
resourcePolicies,
rolePolicies,
roles,
userPolicies,
type ResourcePolicy
} from "@server/db";
import { and, eq } from "drizzle-orm";
import logger from "@server/logger";
import { getUniqueResourcePolicyName } from "@server/db/names";
import response from "@server/lib/response";
const createResourcePolicyParamsSchema = z.strictObject({
orgId: z.string()
});
const createResourcePolicyBodySchema = z.strictObject({
name: z.string().min(1).max(255),
sso: z.boolean(),
skipToIdpId: z.string().optional(),
roleIds: z.array(z.string()).optional().default([]),
userIds: z.array(z.string()).optional().default([])
});
registry.registerPath({
method: "post",
path: "/org/{orgId}/resource-policy",
description: "Create a resource.",
tags: [OpenAPITags.Org, OpenAPITags.Resource],
request: {
params: createResourcePolicyParamsSchema,
body: {
content: {
"application/json": {
schema: createResourcePolicyParamsSchema
}
}
}
},
responses: {}
});
export async function createResourcePolicy(
req: Request,
res: Response,
next: NextFunction
) {
try {
// Validate request params
const parsedParams = createResourcePolicyParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId } = parsedParams.data;
if (req.user && !req.userOrgRoleId) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);
}
// get the org
const org = await db
.select()
.from(orgs)
.where(eq(orgs.orgId, orgId))
.limit(1);
if (org.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Organization with ID ${orgId} not found`
)
);
}
const parsedBody = createResourcePolicyBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { name, sso, userIds, roleIds, skipToIdpId } = parsedBody.data;
const isAuthEnabeld = sso; // other conditions will follow
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."
)
);
}
const adminRole = await db
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (adminRole.length === 0) {
return next(
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
);
}
const niceId = await getUniqueResourcePolicyName(orgId);
const policy = await db.transaction(async (trx) => {
const [newPolicy] = await trx
.insert(resourcePolicies)
.values({
niceId,
orgId,
name,
sso
})
.returning();
await trx.insert(rolePolicies).values({
roleId: adminRole[0].roleId,
resourcePolicyId: newPolicy.resourcePolicyId
});
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
// make sure the user can access the policy
await trx.insert(userPolicies).values({
userId: req.user?.userId!,
resourcePolicyId: newPolicy.resourcePolicyId
});
}
return newPolicy;
});
if (!policy) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to create policy"
)
);
}
return response<ResourcePolicy>(res, {
data: policy,
success: true,
error: false,
message: "resource policy created successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,15 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
export * from "./createResourcePolicy";
export * from "./listResourcePolicies";

View File

@@ -1,12 +0,0 @@
import { Request, Response, NextFunction } from "express";
import z from "zod";
const createResourcePolicyParamsSchema = z.strictObject({
orgId: z.string()
});
export async function createResourcePolicy(
req: Request,
res: Response,
next: NextFunction
) {}

View File

@@ -12,5 +12,3 @@
*/ */
export * from "./getMaintenanceInfo"; export * from "./getMaintenanceInfo";
export * from "./listResourcePolicies";
export * from "./createResourcePolicy";

View File

@@ -1,9 +1,8 @@
import { Request, Response, NextFunction } from "express"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { z } from "zod"; import { build } from "@server/build";
import { db, loginPage } from "@server/db";
import { import {
domains, db,
orgDomains, loginPage,
orgs, orgs,
Resource, Resource,
resources, resources,
@@ -11,19 +10,19 @@ import {
roles, roles,
userResources userResources
} from "@server/db"; } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { eq, and } from "drizzle-orm";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { subdomainSchema } from "@server/lib/schemas";
import config from "@server/lib/config";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { getUniqueResourceName } from "@server/db/names"; import { getUniqueResourceName } from "@server/db/names";
import config from "@server/lib/config";
import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { validateAndConstructDomain } from "@server/lib/domainUtils";
import response from "@server/lib/response";
import { subdomainSchema } from "@server/lib/schemas";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const createResourceParamsSchema = z.strictObject({ const createResourceParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -298,7 +297,7 @@ async function createHttpResource(
); );
} }
if (build != "oss") { if (build !== "oss") {
await createCertificate(domainId, fullDomain, db); await createCertificate(domainId, fullDomain, db);
} }

View File

@@ -5,9 +5,14 @@ import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; import type { ListResourcePoliciesResponse } from "@server/routers/resource/types";
import type { PaginationState } from "@tanstack/react-table"; import type { PaginationState } from "@tanstack/react-table";
import { ArrowRight, MoreHorizontal } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTransition } from "react"; import { useTransition } from "react";
import { useDebouncedCallback } from "use-debounce";
import { Button } from "./ui/button";
import { ControlledDataTable } from "./ui/controlled-data-table";
import type { ExtendedColumnDef } from "./ui/data-table"; import type { ExtendedColumnDef } from "./ui/data-table";
import { import {
DropdownMenu, DropdownMenu,
@@ -15,12 +20,6 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from "./ui/dropdown-menu"; } from "./ui/dropdown-menu";
import { Button } from "./ui/button";
import { MoreHorizontal, ArrowRight } from "lucide-react";
import Link from "next/link";
import { ControlledDataTable } from "./ui/controlled-data-table";
import { useDebouncedCallback } from "use-debounce";
import { Badge } from "./ui/badge";
type ResourcePolicyRow = ListResourcePoliciesResponse["policies"][number]; type ResourcePolicyRow = ListResourcePoliciesResponse["policies"][number];

View File

@@ -42,6 +42,11 @@ import {
PolicyUsersRolesSection PolicyUsersRolesSection
} from "./ResourcePolicySubForms"; } from "./ResourcePolicySubForms";
import { type PolicyFormValues, createPolicySchema } from "."; import { type PolicyFormValues, createPolicySchema } from ".";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { orgs, type ResourcePolicy } from "@server/db";
import type { AxiosResponse } from "axios";
import { useRouter } from "next/navigation";
// ─── CreatePolicyForm ───────────────────────────────────────────────────────── // ─── CreatePolicyForm ─────────────────────────────────────────────────────────
@@ -51,9 +56,12 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) {
const { org } = useOrgContext(); const { org } = useOrgContext();
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext(); const { env } = useEnvContext();
const api = createApiClient({ env });
const [, formAction, isSubmitting] = useActionState(onSubmit, null); const [, formAction, isSubmitting] = useActionState(onSubmit, null);
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
const router = useRouter();
const isMaxmindAvailable = !!( const isMaxmindAvailable = !!(
env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0 env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0
); );
@@ -96,6 +104,52 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) {
const isValid = await form.trigger(); const isValid = await form.trigger();
if (!isValid) return; if (!isValid) return;
const payload = form.getValues();
try {
const res = await api
.post<AxiosResponse<ResourcePolicy>>(
`/org/${org.org.orgId}/resource-policy/`,
{
name: payload.name,
sso: payload.sso,
roleIds: payload.roles.map((r) => r.id),
userIds: payload.users.map((u) => u.id)
}
)
.catch((e) => {
toast({
variant: "destructive",
title: t("policyErrorCreate"),
description: formatAxiosError(
e,
t("policyErrorCreateDescription")
)
});
});
if (res && res.status === 201) {
const id = res.data.data.resourcePolicyId;
const niceId = res.data.data.niceId;
router.push(`/${org.org.orgId}/settings/policies/resources/`);
// should redirect to the details page
// router.push(
// `/${org.org.orgId}/settings/policies/resources/${niceId}`
// );
toast({
title: t("success"),
description: t("policyCreatedSuccess")
});
}
} catch (e) {
toast({
variant: "destructive",
title: t("policyErrorCreate"),
description: t("policyErrorCreateMessageDescription")
});
}
} }
const allRoles = useMemo( const allRoles = useMemo(