mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-04 19:51:29 +02:00
Add policy to blueprints
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
ClientResourcesResults,
|
ClientResourcesResults,
|
||||||
updateClientResources
|
updateClientResources
|
||||||
} from "./clientResources";
|
} from "./clientResources";
|
||||||
|
import { updateResourcePolicies } from "./resourcePolicies";
|
||||||
import { BlueprintSource } from "@server/routers/blueprints/types";
|
import { BlueprintSource } from "@server/routers/blueprints/types";
|
||||||
import { stringify as stringifyYaml } from "yaml";
|
import { stringify as stringifyYaml } from "yaml";
|
||||||
import { generateName } from "@server/db/names";
|
import { generateName } from "@server/db/names";
|
||||||
@@ -56,6 +57,8 @@ export async function applyBlueprint({
|
|||||||
let proxyResourcesResults: ProxyResourcesResults = [];
|
let proxyResourcesResults: ProxyResourcesResults = [];
|
||||||
let clientResourcesResults: ClientResourcesResults = [];
|
let clientResourcesResults: ClientResourcesResults = [];
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
|
await updateResourcePolicies(orgId, config, trx);
|
||||||
|
|
||||||
proxyResourcesResults = await updateProxyResources(
|
proxyResourcesResults = await updateProxyResources(
|
||||||
orgId,
|
orgId,
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -0,0 +1,645 @@
|
|||||||
|
import {
|
||||||
|
db,
|
||||||
|
idp,
|
||||||
|
idpOrg,
|
||||||
|
resourcePolicies,
|
||||||
|
resourcePolicyHeaderAuth,
|
||||||
|
resourcePolicyPassword,
|
||||||
|
resourcePolicyPincode,
|
||||||
|
resourcePolicyRules,
|
||||||
|
resourcePolicyWhiteList,
|
||||||
|
rolePolicies,
|
||||||
|
roles,
|
||||||
|
Transaction,
|
||||||
|
userOrgs,
|
||||||
|
userPolicies,
|
||||||
|
users
|
||||||
|
} from "@server/db";
|
||||||
|
import { eq, and, or } from "drizzle-orm";
|
||||||
|
import { Config, ResourcePolicyData } from "./types";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { getUniqueResourcePolicyName } from "@server/db/names";
|
||||||
|
import { hashPassword } from "@server/auth/password";
|
||||||
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
|
import { tierMatrix } from "../billing/tierMatrix";
|
||||||
|
|
||||||
|
export type ResourcePoliciesResults = {
|
||||||
|
resourcePolicyId: number;
|
||||||
|
niceId: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export async function updateResourcePolicies(
|
||||||
|
orgId: string,
|
||||||
|
config: Config,
|
||||||
|
trx: Transaction
|
||||||
|
): Promise<ResourcePoliciesResults> {
|
||||||
|
const results: ResourcePoliciesResults = [];
|
||||||
|
|
||||||
|
for (const [policyNiceId, policyData] of Object.entries(
|
||||||
|
config["resource-policies"]
|
||||||
|
)) {
|
||||||
|
const isLicensed = await isLicensedOrSubscribed(
|
||||||
|
orgId,
|
||||||
|
tierMatrix.resourcePolicies
|
||||||
|
);
|
||||||
|
if (!isLicensed) {
|
||||||
|
throw new Error(
|
||||||
|
"Your current subscription does not support shared resource policies. Please upgrade to access this feature."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rules
|
||||||
|
for (const rule of policyData.rules) {
|
||||||
|
if (rule.match === "cidr" && !isValidCIDR(rule.value)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid CIDR provided in resource policy '${policyNiceId}': ${rule.value}`
|
||||||
|
);
|
||||||
|
} else if (rule.match === "ip" && !isValidIP(rule.value)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid IP provided in resource policy '${policyNiceId}': ${rule.value}`
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
rule.match === "path" &&
|
||||||
|
!isValidUrlGlobPattern(rule.value)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid URL glob pattern provided in resource policy '${policyNiceId}': ${rule.value}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate auto-login-idp if provided
|
||||||
|
if (policyData["auto-login-idp"]) {
|
||||||
|
const [provider] = await trx
|
||||||
|
.select()
|
||||||
|
.from(idp)
|
||||||
|
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(idp.idpId, policyData["auto-login-idp"]),
|
||||||
|
eq(idpOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error(
|
||||||
|
`Identity provider not found for policy '${policyNiceId}' in this organization`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the admin role
|
||||||
|
const [adminRole] = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!adminRole) {
|
||||||
|
throw new Error("Admin role not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find existing policy by niceId and orgId
|
||||||
|
const [existingPolicy] = await trx
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourcePolicies.niceId, policyNiceId),
|
||||||
|
eq(resourcePolicies.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let resourcePolicyId: number;
|
||||||
|
|
||||||
|
if (existingPolicy) {
|
||||||
|
// Update the existing policy
|
||||||
|
await trx
|
||||||
|
.update(resourcePolicies)
|
||||||
|
.set({
|
||||||
|
name: policyData.name,
|
||||||
|
sso: policyData.sso ?? true,
|
||||||
|
idpId: policyData["auto-login-idp"] ?? null,
|
||||||
|
emailWhitelistEnabled:
|
||||||
|
policyData["email-whitelist-enabled"] ??
|
||||||
|
policyData["whitelist-users"].length > 0,
|
||||||
|
applyRules:
|
||||||
|
policyData["apply-rules"] || policyData.rules.length > 0
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
existingPolicy.resourcePolicyId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
resourcePolicyId = existingPolicy.resourcePolicyId;
|
||||||
|
|
||||||
|
// Sync password
|
||||||
|
await trx
|
||||||
|
.delete(resourcePolicyPassword)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePolicyPassword.resourcePolicyId,
|
||||||
|
resourcePolicyId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (policyData.password) {
|
||||||
|
const passwordHash = await hashPassword(policyData.password);
|
||||||
|
await trx.insert(resourcePolicyPassword).values({
|
||||||
|
resourcePolicyId,
|
||||||
|
passwordHash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync pincode
|
||||||
|
await trx
|
||||||
|
.delete(resourcePolicyPincode)
|
||||||
|
.where(
|
||||||
|
eq(resourcePolicyPincode.resourcePolicyId, resourcePolicyId)
|
||||||
|
);
|
||||||
|
if (policyData.pincode) {
|
||||||
|
const pincodeHash = await hashPassword(policyData.pincode);
|
||||||
|
await trx.insert(resourcePolicyPincode).values({
|
||||||
|
resourcePolicyId,
|
||||||
|
pincodeHash,
|
||||||
|
digitLength: 6
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync header auth
|
||||||
|
await trx
|
||||||
|
.delete(resourcePolicyHeaderAuth)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||||
|
resourcePolicyId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (policyData["basic-auth"]) {
|
||||||
|
const basicAuth = policyData["basic-auth"];
|
||||||
|
const headerAuthHash = await hashPassword(
|
||||||
|
Buffer.from(
|
||||||
|
`${basicAuth.user}:${basicAuth.password}`
|
||||||
|
).toString("base64")
|
||||||
|
);
|
||||||
|
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||||
|
resourcePolicyId,
|
||||||
|
headerAuthHash,
|
||||||
|
extendedCompatibility:
|
||||||
|
basicAuth["extended-compatibility"] ?? true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync SSO roles
|
||||||
|
await syncRolePolicies(
|
||||||
|
resourcePolicyId,
|
||||||
|
policyData["sso-roles"],
|
||||||
|
orgId,
|
||||||
|
adminRole.roleId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sync SSO users
|
||||||
|
await syncUserPolicies(
|
||||||
|
resourcePolicyId,
|
||||||
|
policyData["sso-users"],
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sync whitelist users
|
||||||
|
await syncWhitelistPolicyUsers(
|
||||||
|
resourcePolicyId,
|
||||||
|
policyData["whitelist-users"],
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sync rules
|
||||||
|
await syncPolicyRules(resourcePolicyId, policyData.rules, trx);
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Updated resource policy ${resourcePolicyId} (${policyNiceId})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Create a new policy
|
||||||
|
const [newPolicy] = await trx
|
||||||
|
.insert(resourcePolicies)
|
||||||
|
.values({
|
||||||
|
niceId: policyNiceId,
|
||||||
|
orgId,
|
||||||
|
name: policyData.name,
|
||||||
|
sso: policyData.sso ?? true,
|
||||||
|
idpId: policyData["auto-login-idp"] ?? null,
|
||||||
|
emailWhitelistEnabled:
|
||||||
|
policyData["email-whitelist-enabled"] ??
|
||||||
|
policyData["whitelist-users"].length > 0,
|
||||||
|
applyRules:
|
||||||
|
policyData["apply-rules"] ||
|
||||||
|
policyData.rules.length > 0,
|
||||||
|
scope: "global"
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
resourcePolicyId = newPolicy.resourcePolicyId;
|
||||||
|
|
||||||
|
// Always add admin role
|
||||||
|
await trx.insert(rolePolicies).values({
|
||||||
|
roleId: adminRole.roleId,
|
||||||
|
resourcePolicyId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add SSO roles
|
||||||
|
await addRolePolicies(
|
||||||
|
resourcePolicyId,
|
||||||
|
policyData["sso-roles"],
|
||||||
|
orgId,
|
||||||
|
adminRole.roleId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add SSO users
|
||||||
|
await addUserPolicies(
|
||||||
|
resourcePolicyId,
|
||||||
|
policyData["sso-users"],
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add password
|
||||||
|
if (policyData.password) {
|
||||||
|
const passwordHash = await hashPassword(policyData.password);
|
||||||
|
await trx.insert(resourcePolicyPassword).values({
|
||||||
|
resourcePolicyId,
|
||||||
|
passwordHash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pincode
|
||||||
|
if (policyData.pincode) {
|
||||||
|
const pincodeHash = await hashPassword(policyData.pincode);
|
||||||
|
await trx.insert(resourcePolicyPincode).values({
|
||||||
|
resourcePolicyId,
|
||||||
|
pincodeHash,
|
||||||
|
digitLength: 6
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add header auth
|
||||||
|
if (policyData["basic-auth"]) {
|
||||||
|
const basicAuth = policyData["basic-auth"];
|
||||||
|
const headerAuthHash = await hashPassword(
|
||||||
|
Buffer.from(
|
||||||
|
`${basicAuth.user}:${basicAuth.password}`
|
||||||
|
).toString("base64")
|
||||||
|
);
|
||||||
|
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||||
|
resourcePolicyId,
|
||||||
|
headerAuthHash,
|
||||||
|
extendedCompatibility:
|
||||||
|
basicAuth["extended-compatibility"] ?? true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add whitelist users
|
||||||
|
if (policyData["whitelist-users"].length > 0) {
|
||||||
|
await trx.insert(resourcePolicyWhiteList).values(
|
||||||
|
policyData["whitelist-users"].map((email) => ({
|
||||||
|
email,
|
||||||
|
resourcePolicyId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add rules
|
||||||
|
if (policyData.rules.length > 0) {
|
||||||
|
await trx.insert(resourcePolicyRules).values(
|
||||||
|
policyData.rules.map((rule, index) => ({
|
||||||
|
resourcePolicyId,
|
||||||
|
action: getRuleAction(rule.action),
|
||||||
|
match: getRuleMatch(rule.match),
|
||||||
|
value: rule.value,
|
||||||
|
priority: rule.priority ?? index + 1,
|
||||||
|
enabled: rule.enabled ?? true
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Created resource policy ${resourcePolicyId} (${policyNiceId})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ resourcePolicyId, niceId: policyNiceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRuleAction(input: string): "ACCEPT" | "DROP" | "PASS" {
|
||||||
|
if (input === "allow") return "ACCEPT";
|
||||||
|
if (input === "deny") return "DROP";
|
||||||
|
return "PASS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRuleMatch(input: string): "CIDR" | "IP" | "PATH" {
|
||||||
|
return input.toUpperCase() as "CIDR" | "IP" | "PATH";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncRolePolicies(
|
||||||
|
policyId: number,
|
||||||
|
ssoRoles: string[],
|
||||||
|
orgId: string,
|
||||||
|
adminRoleId: number,
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
const existingRolePolicies = await trx
|
||||||
|
.select()
|
||||||
|
.from(rolePolicies)
|
||||||
|
.where(eq(rolePolicies.resourcePolicyId, policyId));
|
||||||
|
|
||||||
|
for (const roleName of ssoRoles) {
|
||||||
|
const [role] = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.name, roleName), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
logger.warn(
|
||||||
|
`Role '${roleName}' not found in org '${orgId}', skipping`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.isAdmin) {
|
||||||
|
continue; // admin role is always included, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyExists = existingRolePolicies.some(
|
||||||
|
(rp) => rp.roleId === role.roleId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!alreadyExists) {
|
||||||
|
await trx.insert(rolePolicies).values({
|
||||||
|
roleId: role.roleId,
|
||||||
|
resourcePolicyId: policyId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove roles no longer in the list (except admin)
|
||||||
|
for (const existingRolePolicy of existingRolePolicies) {
|
||||||
|
if (existingRolePolicy.roleId === adminRoleId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [role] = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(eq(roles.roleId, existingRolePolicy.roleId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (role?.isAdmin) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role && !ssoRoles.includes(role.name)) {
|
||||||
|
await trx
|
||||||
|
.delete(rolePolicies)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(rolePolicies.resourcePolicyId, policyId),
|
||||||
|
eq(rolePolicies.roleId, existingRolePolicy.roleId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addRolePolicies(
|
||||||
|
policyId: number,
|
||||||
|
ssoRoles: string[],
|
||||||
|
orgId: string,
|
||||||
|
adminRoleId: number,
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
for (const roleName of ssoRoles) {
|
||||||
|
const [role] = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.name, roleName), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
logger.warn(
|
||||||
|
`Role '${roleName}' not found in org '${orgId}', skipping`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.isAdmin) {
|
||||||
|
continue; // admin already added
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.insert(rolePolicies).values({
|
||||||
|
roleId: role.roleId,
|
||||||
|
resourcePolicyId: policyId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncUserPolicies(
|
||||||
|
policyId: number,
|
||||||
|
ssoUsers: string[],
|
||||||
|
orgId: string,
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
const existingUserPolicies = await trx
|
||||||
|
.select()
|
||||||
|
.from(userPolicies)
|
||||||
|
.where(eq(userPolicies.resourcePolicyId, policyId));
|
||||||
|
|
||||||
|
for (const username of ssoUsers) {
|
||||||
|
const [user] = await trx
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
or(eq(users.username, username), eq(users.email, username)),
|
||||||
|
eq(userOrgs.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
logger.warn(
|
||||||
|
`User '${username}' not found in org '${orgId}', skipping`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyExists = existingUserPolicies.some(
|
||||||
|
(up) => up.userId === user.user.userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!alreadyExists) {
|
||||||
|
await trx.insert(userPolicies).values({
|
||||||
|
userId: user.user.userId,
|
||||||
|
resourcePolicyId: policyId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove users no longer in the list
|
||||||
|
for (const existingUserPolicy of existingUserPolicies) {
|
||||||
|
const [user] = await trx
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(users.userId, existingUserPolicy.userId),
|
||||||
|
eq(userOrgs.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (
|
||||||
|
user &&
|
||||||
|
user.user.username &&
|
||||||
|
!ssoUsers.includes(user.user.username) &&
|
||||||
|
!ssoUsers.includes(user.user.email ?? "")
|
||||||
|
) {
|
||||||
|
await trx
|
||||||
|
.delete(userPolicies)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userPolicies.resourcePolicyId, policyId),
|
||||||
|
eq(userPolicies.userId, existingUserPolicy.userId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addUserPolicies(
|
||||||
|
policyId: number,
|
||||||
|
ssoUsers: string[],
|
||||||
|
orgId: string,
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
for (const username of ssoUsers) {
|
||||||
|
const [user] = await trx
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
or(eq(users.username, username), eq(users.email, username)),
|
||||||
|
eq(userOrgs.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
logger.warn(
|
||||||
|
`User '${username}' not found in org '${orgId}', skipping`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.insert(userPolicies).values({
|
||||||
|
userId: user.user.userId,
|
||||||
|
resourcePolicyId: policyId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncWhitelistPolicyUsers(
|
||||||
|
policyId: number,
|
||||||
|
whitelistUsers: string[],
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
const existingWhitelist = await trx
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicyWhiteList)
|
||||||
|
.where(eq(resourcePolicyWhiteList.resourcePolicyId, policyId));
|
||||||
|
|
||||||
|
for (const email of whitelistUsers) {
|
||||||
|
const alreadyExists = existingWhitelist.some((w) => w.email === email);
|
||||||
|
|
||||||
|
if (!alreadyExists) {
|
||||||
|
await trx.insert(resourcePolicyWhiteList).values({
|
||||||
|
email,
|
||||||
|
resourcePolicyId: policyId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const existingEntry of existingWhitelist) {
|
||||||
|
if (!whitelistUsers.includes(existingEntry.email)) {
|
||||||
|
await trx
|
||||||
|
.delete(resourcePolicyWhiteList)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourcePolicyWhiteList.resourcePolicyId, policyId),
|
||||||
|
eq(resourcePolicyWhiteList.email, existingEntry.email)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncPolicyRules(
|
||||||
|
policyId: number,
|
||||||
|
rules: ResourcePolicyData["rules"],
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
const existingRules = await trx
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicyRules)
|
||||||
|
.where(eq(resourcePolicyRules.resourcePolicyId, policyId))
|
||||||
|
.orderBy(resourcePolicyRules.priority);
|
||||||
|
|
||||||
|
for (const [index, rule] of rules.entries()) {
|
||||||
|
const intendedPriority = rule.priority ?? index + 1;
|
||||||
|
const existingRule = existingRules[index];
|
||||||
|
|
||||||
|
if (existingRule) {
|
||||||
|
await trx
|
||||||
|
.update(resourcePolicyRules)
|
||||||
|
.set({
|
||||||
|
action: getRuleAction(rule.action),
|
||||||
|
match: getRuleMatch(rule.match),
|
||||||
|
value: rule.value,
|
||||||
|
priority: intendedPriority,
|
||||||
|
enabled: rule.enabled ?? true
|
||||||
|
})
|
||||||
|
.where(eq(resourcePolicyRules.ruleId, existingRule.ruleId));
|
||||||
|
} else {
|
||||||
|
await trx.insert(resourcePolicyRules).values({
|
||||||
|
resourcePolicyId: policyId,
|
||||||
|
action: getRuleAction(rule.action),
|
||||||
|
match: getRuleMatch(rule.match),
|
||||||
|
value: rule.value,
|
||||||
|
priority: intendedPriority,
|
||||||
|
enabled: rule.enabled ?? true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove extra rules
|
||||||
|
if (existingRules.length > rules.length) {
|
||||||
|
const rulesToDelete = existingRules.slice(rules.length);
|
||||||
|
for (const rule of rulesToDelete) {
|
||||||
|
await trx
|
||||||
|
.delete(resourcePolicyRules)
|
||||||
|
.where(eq(resourcePolicyRules.ruleId, rule.ruleId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -507,6 +507,85 @@ export const PrivateResourceSchema = z
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ResourcePolicyRuleSchema = z
|
||||||
|
.object({
|
||||||
|
action: z.enum(["allow", "deny", "pass"]),
|
||||||
|
match: z.enum(["cidr", "path", "ip"]),
|
||||||
|
value: z.coerce.string(),
|
||||||
|
priority: z.int().optional(),
|
||||||
|
enabled: z.boolean().optional().default(true)
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(rule) => {
|
||||||
|
if (rule.match === "ip") {
|
||||||
|
return z.union([z.ipv4(), z.ipv6()]).safeParse(rule.value)
|
||||||
|
.success;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ["value"],
|
||||||
|
message: "Value must be a valid IP address when match is 'ip'"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(rule) => {
|
||||||
|
if (rule.match === "cidr") {
|
||||||
|
return z.union([z.cidrv4(), z.cidrv6()]).safeParse(rule.value)
|
||||||
|
.success;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ["value"],
|
||||||
|
message: "Value must be a valid CIDR notation when match is 'cidr'"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ResourcePolicySchema = z.object({
|
||||||
|
name: z.string().min(1).max(255),
|
||||||
|
sso: z.boolean().optional().default(true),
|
||||||
|
"auto-login-idp": z.int().positive().optional().nullable(),
|
||||||
|
"sso-roles": z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.default([])
|
||||||
|
.refine((roles) => !roles.includes("Admin"), {
|
||||||
|
error: "Admin role cannot be included in sso-roles"
|
||||||
|
}),
|
||||||
|
"sso-users": z.array(z.string()).optional().default([]),
|
||||||
|
password: z.string().min(4).max(100).optional().nullable(),
|
||||||
|
pincode: z
|
||||||
|
.string()
|
||||||
|
.regex(/^\d{6}$/)
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
"basic-auth": z
|
||||||
|
.object({
|
||||||
|
user: z.string().min(4).max(100),
|
||||||
|
password: z.string().min(4).max(100),
|
||||||
|
"extended-compatibility": z.boolean().default(true)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
"email-whitelist-enabled": z.boolean().optional().default(false),
|
||||||
|
"whitelist-users": z
|
||||||
|
.array(
|
||||||
|
z.email().or(
|
||||||
|
z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, {
|
||||||
|
error: "Invalid email address. Wildcard (*) must be the entire local part."
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.max(50)
|
||||||
|
.transform((v) => v.map((e) => e.toLowerCase()))
|
||||||
|
.optional()
|
||||||
|
.default([]),
|
||||||
|
"apply-rules": z.boolean().optional().default(false),
|
||||||
|
rules: z.array(ResourcePolicyRuleSchema).optional().default([])
|
||||||
|
});
|
||||||
|
export type ResourcePolicyData = z.infer<typeof ResourcePolicySchema>;
|
||||||
|
|
||||||
// Schema for the entire configuration object
|
// Schema for the entire configuration object
|
||||||
export const ConfigSchema = z
|
export const ConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -526,6 +605,10 @@ export const ConfigSchema = z
|
|||||||
.record(z.string(), PrivateResourceSchema)
|
.record(z.string(), PrivateResourceSchema)
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
|
"resource-policies": z
|
||||||
|
.record(z.string(), ResourcePolicySchema)
|
||||||
|
.optional()
|
||||||
|
.prefault({}),
|
||||||
sites: z.record(z.string(), SiteSchema).optional().prefault({})
|
sites: z.record(z.string(), SiteSchema).optional().prefault({})
|
||||||
})
|
})
|
||||||
.transform((data) => {
|
.transform((data) => {
|
||||||
@@ -556,6 +639,10 @@ export const ConfigSchema = z
|
|||||||
string,
|
string,
|
||||||
z.infer<typeof PrivateResourceSchema>
|
z.infer<typeof PrivateResourceSchema>
|
||||||
>;
|
>;
|
||||||
|
"resource-policies": Record<
|
||||||
|
string,
|
||||||
|
z.infer<typeof ResourcePolicySchema>
|
||||||
|
>;
|
||||||
sites: Record<string, z.infer<typeof SiteSchema>>;
|
sites: Record<string, z.infer<typeof SiteSchema>>;
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -695,3 +782,4 @@ export type Site = z.infer<typeof SiteSchema>;
|
|||||||
export type Target = z.infer<typeof TargetSchema>;
|
export type Target = z.infer<typeof TargetSchema>;
|
||||||
export type Resource = z.infer<typeof PublicResourceSchema>;
|
export type Resource = z.infer<typeof PublicResourceSchema>;
|
||||||
export type Config = z.infer<typeof ConfigSchema>;
|
export type Config = z.infer<typeof ConfigSchema>;
|
||||||
|
export type BlueprintResourcePolicy = z.infer<typeof ResourcePolicySchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user