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.",
"rulesOrder": "Rules are evaluated by priority in ascending order.",
"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",
"resourceErrorCreateDescription": "An error occurred when creating the resource",
"resourceErrorCreateMessage": "Error creating resource:",

View File

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

View File

@@ -1,6 +1,12 @@
import { join } from "path";
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 { exitNodes, sites } from "@server/db";
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(
orgId: 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 approval from "#private/routers/approvals";
import * as resource from "#private/routers/resource";
import * as policy from "#private/routers/policy";
import {
verifyOrgAccess,
@@ -349,9 +350,19 @@ authenticated.get(
verifyLimits,
verifyUserHasAction(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(
"/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 "./listResourcePolicies";
export * from "./createResourcePolicy";

View File

@@ -1,9 +1,8 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, loginPage } from "@server/db";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { build } from "@server/build";
import {
domains,
orgDomains,
db,
loginPage,
orgs,
Resource,
resources,
@@ -11,19 +10,19 @@ import {
roles,
userResources
} 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 config from "@server/lib/config";
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({
orgId: z.string()
@@ -298,7 +297,7 @@ async function createHttpResource(
);
}
if (build != "oss") {
if (build !== "oss") {
await createCertificate(domainId, fullDomain, db);
}

View File

@@ -5,9 +5,14 @@ import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import type { ListResourcePoliciesResponse } from "@server/routers/resource/types";
import type { PaginationState } from "@tanstack/react-table";
import { ArrowRight, MoreHorizontal } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation";
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 {
DropdownMenu,
@@ -15,12 +20,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger
} 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];

View File

@@ -42,6 +42,11 @@ import {
PolicyUsersRolesSection
} from "./ResourcePolicySubForms";
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 ─────────────────────────────────────────────────────────
@@ -51,9 +56,12 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) {
const { org } = useOrgContext();
const t = useTranslations();
const { env } = useEnvContext();
const api = createApiClient({ env });
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
const { isPaidUser } = usePaidStatus();
const router = useRouter();
const isMaxmindAvailable = !!(
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();
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(