♻️ create table for resource policies associations with users

This commit is contained in:
Fred KISSIE
2026-02-24 03:05:51 +01:00
parent 0e4abdf4b6
commit 335411de4c
8 changed files with 106 additions and 66 deletions

View File

@@ -1251,7 +1251,8 @@
"sidebarResources": "Resources", "sidebarResources": "Resources",
"sidebarProxyResources": "Public", "sidebarProxyResources": "Public",
"sidebarClientResources": "Private", "sidebarClientResources": "Private",
"sidebarResourcePolicies": "Policies", "sidebarPolicies": "Policies",
"sidebarResourcePolicies": "Resources",
"sidebarAccessControl": "Access Control", "sidebarAccessControl": "Access Control",
"sidebarLogsAndAnalytics": "Logs & Analytics", "sidebarLogsAndAnalytics": "Logs & Analytics",
"sidebarUsers": "Users", "sidebarUsers": "Users",

View File

@@ -94,8 +94,10 @@ export const sites = pgTable("sites", {
export const resources = pgTable("resources", { export const resources = pgTable("resources", {
resourceId: serial("resourceId").primaryKey(), resourceId: serial("resourceId").primaryKey(),
resourcePolicyId: integer("resourcePolicyId") resourcePolicyId: integer("resourcePolicyId").references(
.references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), () => resourcePolicies.resourcePolicyId,
{ onDelete: "cascade" }
),
resourceGuid: varchar("resourceGuid", { length: 36 }) resourceGuid: varchar("resourceGuid", { length: 36 })
.unique() .unique()
.notNull() .notNull()
@@ -420,10 +422,7 @@ export const roleResources = pgTable("roleResources", {
.references(() => roles.roleId, { onDelete: "cascade" }), .references(() => roles.roleId, { onDelete: "cascade" }),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" })
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }),
}); });
export const userResources = pgTable("userResources", { export const userResources = pgTable("userResources", {
@@ -432,10 +431,29 @@ export const userResources = pgTable("userResources", {
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, { onDelete: "cascade" }),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" })
});
export const rolePolicies = pgTable("rolePolicies", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
resourcePolicyId: integer("resourcePolicyId") resourcePolicyId: integer("resourcePolicyId")
.notNull() .notNull()
.references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), .references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const userPolicies = pgTable("userPolicies", {
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
}); });
export const userInvites = pgTable("userInvites", { export const userInvites = pgTable("userInvites", {
@@ -460,7 +478,9 @@ export const resourcePincode = pgTable("resourcePincode", {
digitLength: integer("digitLength").notNull(), digitLength: integer("digitLength").notNull(),
resourcePolicyId: integer("resourcePolicyId") resourcePolicyId: integer("resourcePolicyId")
.notNull() .notNull()
.references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), .references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
}); });
export const resourcePassword = pgTable("resourcePassword", { export const resourcePassword = pgTable("resourcePassword", {
@@ -471,7 +491,9 @@ export const resourcePassword = pgTable("resourcePassword", {
passwordHash: varchar("passwordHash").notNull(), passwordHash: varchar("passwordHash").notNull(),
resourcePolicyId: integer("resourcePolicyId") resourcePolicyId: integer("resourcePolicyId")
.notNull() .notNull()
.references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), .references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
}); });
export const resourceHeaderAuth = pgTable("resourceHeaderAuth", { export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
@@ -482,7 +504,9 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
headerAuthHash: varchar("headerAuthHash").notNull(), headerAuthHash: varchar("headerAuthHash").notNull(),
resourcePolicyId: integer("resourcePolicyId") resourcePolicyId: integer("resourcePolicyId")
.notNull() .notNull()
.references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), .references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
}); });
export const resourceHeaderAuthExtendedCompatibility = pgTable( export const resourceHeaderAuthExtendedCompatibility = pgTable(
@@ -496,7 +520,9 @@ export const resourceHeaderAuthExtendedCompatibility = pgTable(
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" }),
resourcePolicyId: integer("resourcePolicyId") resourcePolicyId: integer("resourcePolicyId")
.notNull() .notNull()
.references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), .references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
}),
extendedCompatibilityIsActivated: boolean( extendedCompatibilityIsActivated: boolean(
"extendedCompatibilityIsActivated" "extendedCompatibilityIsActivated"
) )
@@ -571,7 +597,9 @@ export const resourceWhitelist = pgTable("resourceWhitelist", {
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" }),
resourcePolicyId: integer("resourcePolicyId") resourcePolicyId: integer("resourcePolicyId")
.notNull() .notNull()
.references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), .references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
}); });
export const resourceOtp = pgTable("resourceOtp", { export const resourceOtp = pgTable("resourceOtp", {
@@ -581,7 +609,9 @@ export const resourceOtp = pgTable("resourceOtp", {
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" }),
resourcePolicyId: integer("resourcePolicyId") resourcePolicyId: integer("resourcePolicyId")
.notNull() .notNull()
.references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), .references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
}),
email: varchar("email").notNull(), email: varchar("email").notNull(),
otpHash: varchar("otpHash").notNull(), otpHash: varchar("otpHash").notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull() expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
@@ -599,7 +629,9 @@ export const resourceRules = pgTable("resourceRules", {
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" }),
resourcePolicyId: integer("resourcePolicyId") resourcePolicyId: integer("resourcePolicyId")
.notNull() .notNull()
.references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), .references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
}),
enabled: boolean("enabled").notNull().default(true), enabled: boolean("enabled").notNull().default(true),
priority: integer("priority").notNull(), priority: integer("priority").notNull(),
action: varchar("action").notNull(), // ACCEPT, DROP, PASS action: varchar("action").notNull(), // ACCEPT, DROP, PASS
@@ -607,21 +639,40 @@ export const resourceRules = pgTable("resourceRules", {
value: varchar("value").notNull() value: varchar("value").notNull()
}); });
export const policyRules = pgTable("policyRules", {
ruleId: serial("ruleId").primaryKey(),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
}),
enabled: boolean("enabled").notNull().default(true),
priority: integer("priority").notNull(),
action: varchar("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
match: varchar("match").$type<"CIDR" | "PATH" | "IP">().notNull(),
value: varchar("value").notNull()
});
export const resourcePolicies = pgTable("resourcePolicies", { export const resourcePolicies = pgTable("resourcePolicies", {
resourcePolicyId: serial('resourcePolicyId').primaryKey(), resourcePolicyId: serial("resourcePolicyId").primaryKey(),
sso: boolean("sso").notNull().default(true), sso: boolean("sso").notNull().default(true),
emailWhitelistEnabled: boolean("emailWhitelistEnabled").notNull().default(false), scope: varchar("scope")
.$type<"global" | "resource">()
.notNull()
.default("global"),
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
.notNull()
.default(false),
idpId: integer("idpId").references(() => idp.idpId, { idpId: integer("idpId").references(() => idp.idpId, {
onDelete: "set null" onDelete: "set null"
}), }),
niceId: text("niceId").notNull(), niceId: text("niceId").notNull(),
isDefault: boolean("isDefault").notNull().default(true),
name: varchar("name").notNull(), name: varchar("name").notNull(),
orgId: varchar("orgId") orgId: varchar("orgId")
.references(() => orgs.orgId, { .references(() => orgs.orgId, {
onDelete: "cascade" onDelete: "cascade"
}) })
.notNull(), .notNull()
}); });
export const supporterKey = pgTable("supporterKey", { export const supporterKey = pgTable("supporterKey", {

View File

@@ -11,7 +11,7 @@
* This file is not licensed under the AGPLv3. * This file is not licensed under the AGPLv3.
*/ */
import { db, resourcePolicies, roleResources, userResources } from "@server/db"; import { db, resourcePolicies, rolePolicies, userPolicies } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
@@ -51,8 +51,7 @@ function queryResourcePoliciesBase() {
resourcePolicyId: resourcePolicies.resourcePolicyId, resourcePolicyId: resourcePolicies.resourcePolicyId,
name: resourcePolicies.name, name: resourcePolicies.name,
niceId: resourcePolicies.niceId, niceId: resourcePolicies.niceId,
orgId: resourcePolicies.orgId, orgId: resourcePolicies.orgId
isDefault: resourcePolicies.isDefault
}) })
.from(resourcePolicies); .from(resourcePolicies);
} }
@@ -124,20 +123,20 @@ export async function listResourcePolicies(
if (req.user) { if (req.user) {
accessibleResourcePolicies = await db accessibleResourcePolicies = await db
.select({ .select({
resourcePolicyId: sql<number>`COALESCE(${userResources.resourcePolicyId}, ${roleResources.resourcePolicyId})` resourcePolicyId: sql<number>`COALESCE(${userPolicies.resourcePolicyId}, ${rolePolicies.resourcePolicyId})`
}) })
.from(userResources) .from(userPolicies)
.fullJoin( .fullJoin(
roleResources, rolePolicies,
eq( eq(
userResources.resourcePolicyId, userPolicies.resourcePolicyId,
roleResources.resourcePolicyId rolePolicies.resourcePolicyId
) )
) )
.where( .where(
or( or(
eq(userResources.userId, req.user!.userId), eq(userPolicies.userId, req.user!.userId),
eq(roleResources.roleId, req.userOrgRoleId!) eq(rolePolicies.roleId, req.userOrgRoleId!)
) )
); );
} else { } else {
@@ -159,7 +158,8 @@ export async function listResourcePolicies(
resourcePolicies.resourcePolicyId, resourcePolicies.resourcePolicyId,
accessibleResourceIds accessibleResourceIds
), ),
eq(resourcePolicies.orgId, orgId) eq(resourcePolicies.orgId, orgId),
eq(resourcePolicies.scope, "global")
) )
]; ];

View File

@@ -14,9 +14,6 @@ export type GetMaintenanceInfoResponse = {
export type ListResourcePoliciesResponse = PaginatedResponse<{ export type ListResourcePoliciesResponse = PaginatedResponse<{
policies: Array< policies: Array<
Pick< Pick<ResourcePolicy, "resourcePolicyId" | "niceId" | "name" | "orgId">
ResourcePolicy,
"resourcePolicyId" | "niceId" | "name" | "orgId" | "isDefault"
>
>; >;
}>; }>;

View File

@@ -7,6 +7,7 @@ import {
CreditCard, CreditCard,
Fingerprint, Fingerprint,
Globe, Globe,
GlobeIcon,
GlobeLock, GlobeLock,
KeyRound, KeyRound,
Laptop, Laptop,
@@ -63,18 +64,7 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
title: "sidebarClientResources", title: "sidebarClientResources",
href: "/{orgId}/settings/resources/client", href: "/{orgId}/settings/resources/client",
icon: <GlobeLock className="size-4 flex-none" /> icon: <GlobeLock className="size-4 flex-none" />
}, }
...(build !== "oss"
? [
{
title: "sidebarResourcePolicies",
href: "/{orgId}/settings/resources/policies",
icon: (
<ShieldIcon className="size-4 flex-none" />
)
}
]
: [])
] ]
}, },
{ {
@@ -133,6 +123,24 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
href: "/{orgId}/settings/access/roles", href: "/{orgId}/settings/access/roles",
icon: <Users className="size-4 flex-none" /> icon: <Users className="size-4 flex-none" />
}, },
...(build !== "oss"
? [
{
title: "sidebarPolicies",
icon: <ShieldIcon className="size-4 flex-none" />,
items: [
{
title: "sidebarResourcePolicies",
href: "/{orgId}/settings/policies/resources",
icon: (
<GlobeIcon className="size-4 flex-none" />
)
}
]
}
]
: []),
// PaidFeaturesAlert // PaidFeaturesAlert
...((build === "oss" && !env?.flags.disableEnterpriseFeatures) || ...((build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
build === "saas" || build === "saas" ||

View File

@@ -72,24 +72,7 @@ export function ResourcePoliciesTable({
enableHiding: false, enableHiding: false,
friendlyName: t("name"), friendlyName: t("name"),
header: () => <span className="p-3">{t("name")}</span>, header: () => <span className="p-3">{t("name")}</span>,
cell({ row }) { cell: ({ row }) => <span>{row.original.name}</span>
const r = row.original;
return (
<div className="flex items-center gap-2">
<span>{r.name}</span>
{r.isDefault && (
<>
<Badge
variant="outlinePrimary"
className="flex items-center gap-1"
>
{t("resourcePoliciesDefaultBadgeText")}
</Badge>
</>
)}
</div>
);
}
}, },
{ {
id: "niceId", id: "niceId",
@@ -183,7 +166,7 @@ export function ResourcePoliciesTable({
onAdd={() => onAdd={() =>
startNavigation(() => startNavigation(() =>
router.push( router.push(
`/${orgId}/settings/resources/policies/create` `/${orgId}/settings/policies/resources/create`
) )
) )
} }