mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-22 00:35:22 +00:00
Add policies to blueprints
This commit is contained in:
@@ -16,7 +16,15 @@ import {
|
||||
Transaction,
|
||||
userOrgs,
|
||||
userResources,
|
||||
users
|
||||
users,
|
||||
resourcePolicies,
|
||||
resourcePolicyPassword,
|
||||
resourcePolicyPincode,
|
||||
resourcePolicyHeaderAuth,
|
||||
resourcePolicyRules,
|
||||
resourcePolicyWhiteList,
|
||||
rolePolicies,
|
||||
userPolicies
|
||||
} from "@server/db";
|
||||
import { resources, targets, sites } from "@server/db";
|
||||
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
|
||||
@@ -30,6 +38,7 @@ import logger from "@server/logger";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { pickPort } from "@server/routers/target/helpers";
|
||||
import { resourcePassword } from "@server/db";
|
||||
import { getUniqueResourcePolicyName } from "@server/db/names";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||
import { isValidRegionId } from "@server/db/regions";
|
||||
@@ -242,6 +251,39 @@ export async function updateProxyResources(
|
||||
resourceData.maintenance = undefined;
|
||||
}
|
||||
|
||||
// Look up the admin role (needed for inline policy creation)
|
||||
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`);
|
||||
}
|
||||
|
||||
if (resourceData.policy) {
|
||||
// SHARED POLICY MODE: look up shared policy by niceId
|
||||
const [sharedPolicy] = await trx
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
resourcePolicies.niceId,
|
||||
resourceData.policy
|
||||
),
|
||||
eq(resourcePolicies.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!sharedPolicy) {
|
||||
throw new Error(
|
||||
`Shared policy not found: ${resourceData.policy} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
|
||||
[resource] = await trx
|
||||
.update(resources)
|
||||
.set({
|
||||
@@ -249,7 +291,9 @@ export async function updateProxyResources(
|
||||
protocol: protocol || "tcp",
|
||||
http: http,
|
||||
proxyPort: http ? null : resourceData["proxy-port"],
|
||||
fullDomain: http ? resourceData["full-domain"] : null,
|
||||
fullDomain: http
|
||||
? resourceData["full-domain"]
|
||||
: null,
|
||||
subdomain: domain ? domain.subdomain : null,
|
||||
domainId: domain ? domain.domainId : null,
|
||||
wildcard: domain ? domain.wildcard : false,
|
||||
@@ -259,28 +303,37 @@ export async function updateProxyResources(
|
||||
resourceData.auth?.["auto-login-idp"] || null,
|
||||
ssl: resourceSsl,
|
||||
setHostHeader: resourceData["host-header"] || null,
|
||||
tlsServerName: resourceData["tls-server-name"] || null,
|
||||
tlsServerName:
|
||||
resourceData["tls-server-name"] || null,
|
||||
emailWhitelistEnabled: resourceData.auth?.[
|
||||
"whitelist-users"
|
||||
]
|
||||
? resourceData.auth["whitelist-users"].length > 0
|
||||
? resourceData.auth["whitelist-users"].length >
|
||||
0
|
||||
: false,
|
||||
headers: headers || null,
|
||||
applyRules:
|
||||
resourceData.rules && resourceData.rules.length > 0,
|
||||
resourceData.rules &&
|
||||
resourceData.rules.length > 0,
|
||||
maintenanceModeEnabled:
|
||||
resourceData.maintenance?.enabled,
|
||||
maintenanceModeType: resourceData.maintenance?.type,
|
||||
maintenanceTitle: resourceData.maintenance?.title,
|
||||
maintenanceMessage: resourceData.maintenance?.message,
|
||||
maintenanceMessage:
|
||||
resourceData.maintenance?.message,
|
||||
maintenanceEstimatedTime:
|
||||
resourceData.maintenance?.["estimated-time"]
|
||||
resourceData.maintenance?.["estimated-time"],
|
||||
resourcePolicyId: sharedPolicy.resourcePolicyId
|
||||
})
|
||||
.where(
|
||||
eq(resources.resourceId, existingResource.resourceId)
|
||||
eq(
|
||||
resources.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
// Update OLD resource-level auth tables
|
||||
await trx
|
||||
.delete(resourcePassword)
|
||||
.where(
|
||||
@@ -293,7 +346,6 @@ export async function updateProxyResources(
|
||||
const passwordHash = await hashPassword(
|
||||
resourceData.auth.password
|
||||
);
|
||||
|
||||
await trx.insert(resourcePassword).values({
|
||||
resourceId: existingResource.resourceId,
|
||||
passwordHash
|
||||
@@ -312,7 +364,6 @@ export async function updateProxyResources(
|
||||
const pincodeHash = await hashPassword(
|
||||
resourceData.auth.pincode.toString()
|
||||
);
|
||||
|
||||
await trx.insert(resourcePincode).values({
|
||||
resourceId: existingResource.resourceId,
|
||||
pincodeHash,
|
||||
@@ -328,7 +379,6 @@ export async function updateProxyResources(
|
||||
existingResource.resourceId
|
||||
)
|
||||
);
|
||||
|
||||
await trx
|
||||
.delete(resourceHeaderAuthExtendedCompatibility)
|
||||
.where(
|
||||
@@ -337,14 +387,13 @@ export async function updateProxyResources(
|
||||
existingResource.resourceId
|
||||
)
|
||||
);
|
||||
|
||||
if (resourceData.auth?.["basic-auth"]) {
|
||||
const headerAuthUser =
|
||||
resourceData.auth?.["basic-auth"]?.user;
|
||||
resourceData.auth["basic-auth"]?.user;
|
||||
const headerAuthPassword =
|
||||
resourceData.auth?.["basic-auth"]?.password;
|
||||
resourceData.auth["basic-auth"]?.password;
|
||||
const headerAuthExtendedCompatibility =
|
||||
resourceData.auth?.["basic-auth"]
|
||||
resourceData.auth["basic-auth"]
|
||||
?.extendedCompatibility;
|
||||
if (
|
||||
headerAuthUser &&
|
||||
@@ -362,7 +411,9 @@ export async function updateProxyResources(
|
||||
headerAuthHash
|
||||
}),
|
||||
trx
|
||||
.insert(resourceHeaderAuthExtendedCompatibility)
|
||||
.insert(
|
||||
resourceHeaderAuthExtendedCompatibility
|
||||
)
|
||||
.values({
|
||||
resourceId: existingResource.resourceId,
|
||||
extendedCompatibilityIsActivated:
|
||||
@@ -373,35 +424,87 @@ export async function updateProxyResources(
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["sso-roles"]) {
|
||||
const ssoRoles = resourceData.auth?.["sso-roles"];
|
||||
await syncRoleResources(
|
||||
existingResource.resourceId,
|
||||
ssoRoles,
|
||||
resourceData.auth["sso-roles"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["sso-users"]) {
|
||||
const ssoUsers = resourceData.auth?.["sso-users"];
|
||||
await syncUserResources(
|
||||
existingResource.resourceId,
|
||||
ssoUsers,
|
||||
resourceData.auth["sso-users"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["whitelist-users"]) {
|
||||
const whitelistUsers =
|
||||
resourceData.auth?.["whitelist-users"];
|
||||
await syncWhitelistUsers(
|
||||
existingResource.resourceId,
|
||||
whitelistUsers,
|
||||
resourceData.auth["whitelist-users"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// INLINE POLICY MODE: ensure inline policy exists
|
||||
const inlinePolicyId = await ensureInlinePolicy(
|
||||
existingResource.defaultResourcePolicyId,
|
||||
orgId,
|
||||
resourceNiceId,
|
||||
adminRole.roleId,
|
||||
trx
|
||||
);
|
||||
|
||||
[resource] = await trx
|
||||
.update(resources)
|
||||
.set({
|
||||
name: resourceData.name || "Unnamed Resource",
|
||||
protocol: protocol || "tcp",
|
||||
http: http,
|
||||
proxyPort: http ? null : resourceData["proxy-port"],
|
||||
fullDomain: http
|
||||
? resourceData["full-domain"]
|
||||
: null,
|
||||
subdomain: domain ? domain.subdomain : null,
|
||||
domainId: domain ? domain.domainId : null,
|
||||
wildcard: domain ? domain.wildcard : false,
|
||||
enabled: resourceEnabled,
|
||||
ssl: resourceSsl,
|
||||
setHostHeader: resourceData["host-header"] || null,
|
||||
tlsServerName:
|
||||
resourceData["tls-server-name"] || null,
|
||||
headers: headers || null,
|
||||
maintenanceModeEnabled:
|
||||
resourceData.maintenance?.enabled,
|
||||
maintenanceModeType: resourceData.maintenance?.type,
|
||||
maintenanceTitle: resourceData.maintenance?.title,
|
||||
maintenanceMessage:
|
||||
resourceData.maintenance?.message,
|
||||
maintenanceEstimatedTime:
|
||||
resourceData.maintenance?.["estimated-time"],
|
||||
resourcePolicyId: null,
|
||||
defaultResourcePolicyId: inlinePolicyId
|
||||
})
|
||||
.where(
|
||||
eq(
|
||||
resources.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
// Update inline policy auth fields and policy-level tables
|
||||
await syncInlinePolicyAuth(
|
||||
inlinePolicyId,
|
||||
orgId,
|
||||
resourceData,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const existingResourceTargets = await trx
|
||||
@@ -618,21 +721,28 @@ export async function updateProxyResources(
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceData.policy) {
|
||||
// SHARED POLICY MODE: sync rules into old resourceRules table
|
||||
const existingRules = await trx
|
||||
.select()
|
||||
.from(resourceRules)
|
||||
.where(
|
||||
eq(resourceRules.resourceId, existingResource.resourceId)
|
||||
eq(
|
||||
resourceRules.resourceId,
|
||||
existingResource.resourceId
|
||||
)
|
||||
)
|
||||
.orderBy(resourceRules.priority);
|
||||
|
||||
// Sync rules
|
||||
for (const [index, rule] of resourceData.rules?.entries() || []) {
|
||||
for (const [index, rule] of resourceData.rules?.entries() ||
|
||||
[]) {
|
||||
const intendedPriority = rule.priority ?? index + 1;
|
||||
const existingRule = existingRules[index];
|
||||
if (existingRule) {
|
||||
if (
|
||||
existingRule.action !== getRuleAction(rule.action) ||
|
||||
existingRule.action !==
|
||||
getRuleAction(rule.action) ||
|
||||
existingRule.match !== rule.match.toUpperCase() ||
|
||||
existingRule.value !==
|
||||
getRuleValue(
|
||||
@@ -654,7 +764,10 @@ export async function updateProxyResources(
|
||||
priority: intendedPriority
|
||||
})
|
||||
.where(
|
||||
eq(resourceRules.ruleId, existingRule.ruleId)
|
||||
eq(
|
||||
resourceRules.ruleId,
|
||||
existingRule.ruleId
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -682,6 +795,15 @@ export async function updateProxyResources(
|
||||
.where(eq(resourceRules.ruleId, rule.ruleId));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// INLINE POLICY MODE: sync rules into policy-level table
|
||||
const inlinePolicyId = resource!.defaultResourcePolicyId!;
|
||||
await syncInlinePolicyRules(
|
||||
inlinePolicyId,
|
||||
resourceData.rules || [],
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(`Updated resource ${existingResource.resourceId}`);
|
||||
} else {
|
||||
@@ -704,6 +826,58 @@ export async function updateProxyResources(
|
||||
resourceData.maintenance = undefined;
|
||||
}
|
||||
|
||||
// Look up admin role (needed for inline policy and roleResources)
|
||||
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`);
|
||||
}
|
||||
|
||||
// Always create an inline policy for the resource
|
||||
const policyNiceId = await getUniqueResourcePolicyName(orgId);
|
||||
const [inlinePolicy] = await trx
|
||||
.insert(resourcePolicies)
|
||||
.values({
|
||||
niceId: policyNiceId,
|
||||
orgId,
|
||||
name: `default policy for ${resourceNiceId}`,
|
||||
sso: true,
|
||||
scope: "resource"
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Make the inline policy visible to the admin role
|
||||
await trx.insert(rolePolicies).values({
|
||||
roleId: adminRole.roleId,
|
||||
resourcePolicyId: inlinePolicy.resourcePolicyId
|
||||
});
|
||||
|
||||
// Determine the active shared policy (if provided)
|
||||
let sharedPolicyId: number | null = null;
|
||||
if (resourceData.policy) {
|
||||
const [sharedPolicy] = await trx
|
||||
.select()
|
||||
.from(resourcePolicies)
|
||||
.where(
|
||||
and(
|
||||
eq(resourcePolicies.niceId, resourceData.policy),
|
||||
eq(resourcePolicies.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!sharedPolicy) {
|
||||
throw new Error(
|
||||
`Shared policy not found: ${resourceData.policy} in org ${orgId}`
|
||||
);
|
||||
}
|
||||
sharedPolicyId = sharedPolicy.resourcePolicyId;
|
||||
}
|
||||
|
||||
// Create new resource
|
||||
const [newResource] = await trx
|
||||
.insert(resources)
|
||||
@@ -719,28 +893,51 @@ export async function updateProxyResources(
|
||||
domainId: domain ? domain.domainId : null,
|
||||
wildcard: domain ? domain.wildcard : false,
|
||||
enabled: resourceEnabled,
|
||||
sso: resourceData.auth?.["sso-enabled"] || false,
|
||||
skipToIdpId: resourceData.auth?.["auto-login-idp"] || null,
|
||||
setHostHeader: resourceData["host-header"] || null,
|
||||
tlsServerName: resourceData["tls-server-name"] || null,
|
||||
ssl: resourceSsl,
|
||||
headers: headers || null,
|
||||
applyRules:
|
||||
resourceData.rules && resourceData.rules.length > 0,
|
||||
maintenanceModeEnabled: resourceData.maintenance?.enabled,
|
||||
maintenanceModeType: resourceData.maintenance?.type,
|
||||
maintenanceTitle: resourceData.maintenance?.title,
|
||||
maintenanceMessage: resourceData.maintenance?.message,
|
||||
maintenanceEstimatedTime:
|
||||
resourceData.maintenance?.["estimated-time"]
|
||||
resourceData.maintenance?.["estimated-time"],
|
||||
defaultResourcePolicyId: inlinePolicy.resourcePolicyId,
|
||||
resourcePolicyId: sharedPolicyId,
|
||||
// Only set these resource-level fields when using a shared policy
|
||||
...(sharedPolicyId
|
||||
? {
|
||||
sso: resourceData.auth?.["sso-enabled"] || false,
|
||||
skipToIdpId:
|
||||
resourceData.auth?.["auto-login-idp"] || null,
|
||||
emailWhitelistEnabled: resourceData.auth?.[
|
||||
"whitelist-users"
|
||||
]
|
||||
? resourceData.auth["whitelist-users"]
|
||||
.length > 0
|
||||
: false,
|
||||
applyRules:
|
||||
resourceData.rules &&
|
||||
resourceData.rules.length > 0
|
||||
}
|
||||
: {})
|
||||
})
|
||||
.returning();
|
||||
|
||||
resource = newResource;
|
||||
|
||||
await trx.insert(roleResources).values({
|
||||
roleId: adminRole.roleId,
|
||||
resourceId: newResource.resourceId
|
||||
});
|
||||
|
||||
if (sharedPolicyId) {
|
||||
// SHARED POLICY MODE: update OLD resource-level auth tables
|
||||
if (resourceData.auth?.password) {
|
||||
const passwordHash = await hashPassword(
|
||||
resourceData.auth.password
|
||||
);
|
||||
|
||||
await trx.insert(resourcePassword).values({
|
||||
resourceId: newResource.resourceId,
|
||||
passwordHash
|
||||
@@ -751,7 +948,6 @@ export async function updateProxyResources(
|
||||
const pincodeHash = await hashPassword(
|
||||
resourceData.auth.pincode.toString()
|
||||
);
|
||||
|
||||
await trx.insert(resourcePincode).values({
|
||||
resourceId: newResource.resourceId,
|
||||
pincodeHash,
|
||||
@@ -760,12 +956,12 @@ export async function updateProxyResources(
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["basic-auth"]) {
|
||||
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
|
||||
const headerAuthUser =
|
||||
resourceData.auth["basic-auth"]?.user;
|
||||
const headerAuthPassword =
|
||||
resourceData.auth?.["basic-auth"]?.password;
|
||||
resourceData.auth["basic-auth"]?.password;
|
||||
const headerAuthExtendedCompatibility =
|
||||
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
||||
|
||||
resourceData.auth["basic-auth"]?.extendedCompatibility;
|
||||
if (
|
||||
headerAuthUser &&
|
||||
headerAuthPassword &&
|
||||
@@ -776,7 +972,6 @@ export async function updateProxyResources(
|
||||
`${headerAuthUser}:${headerAuthPassword}`
|
||||
).toString("base64")
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
trx.insert(resourceHeaderAuth).values({
|
||||
resourceId: newResource.resourceId,
|
||||
@@ -793,53 +988,65 @@ export async function updateProxyResources(
|
||||
}
|
||||
}
|
||||
|
||||
resource = newResource;
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
await trx.insert(roleResources).values({
|
||||
roleId: adminRole.roleId,
|
||||
resourceId: newResource.resourceId
|
||||
});
|
||||
|
||||
if (resourceData.auth?.["sso-roles"]) {
|
||||
const ssoRoles = resourceData.auth?.["sso-roles"];
|
||||
await syncRoleResources(
|
||||
newResource.resourceId,
|
||||
ssoRoles,
|
||||
resourceData.auth["sso-roles"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["sso-users"]) {
|
||||
const ssoUsers = resourceData.auth?.["sso-users"];
|
||||
await syncUserResources(
|
||||
newResource.resourceId,
|
||||
ssoUsers,
|
||||
resourceData.auth["sso-users"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceData.auth?.["whitelist-users"]) {
|
||||
const whitelistUsers = resourceData.auth?.["whitelist-users"];
|
||||
await syncWhitelistUsers(
|
||||
newResource.resourceId,
|
||||
whitelistUsers,
|
||||
resourceData.auth["whitelist-users"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
// Rules into OLD resourceRules table
|
||||
for (const [index, rule] of resourceData.rules?.entries() ||
|
||||
[]) {
|
||||
validateRule(rule);
|
||||
await trx.insert(resourceRules).values({
|
||||
resourceId: newResource.resourceId,
|
||||
action: getRuleAction(rule.action),
|
||||
match: rule.match.toUpperCase(),
|
||||
value: getRuleValue(
|
||||
rule.match.toUpperCase(),
|
||||
rule.value
|
||||
),
|
||||
priority: rule.priority ?? index + 1
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// INLINE POLICY MODE: update the inline policy auth fields
|
||||
await syncInlinePolicyAuth(
|
||||
inlinePolicy.resourcePolicyId,
|
||||
orgId,
|
||||
resourceData,
|
||||
trx
|
||||
);
|
||||
|
||||
// Rules into policy-level table
|
||||
await syncInlinePolicyRules(
|
||||
inlinePolicy.resourcePolicyId,
|
||||
resourceData.rules || [],
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
// Create new targets
|
||||
for (const targetData of resourceData.targets) {
|
||||
if (!targetData) {
|
||||
@@ -849,17 +1056,6 @@ export async function updateProxyResources(
|
||||
await createTarget(newResource.resourceId, targetData);
|
||||
}
|
||||
|
||||
for (const [index, rule] of resourceData.rules?.entries() || []) {
|
||||
validateRule(rule);
|
||||
await trx.insert(resourceRules).values({
|
||||
resourceId: newResource.resourceId,
|
||||
action: getRuleAction(rule.action),
|
||||
match: rule.match.toUpperCase(),
|
||||
value: getRuleValue(rule.match.toUpperCase(), rule.value),
|
||||
priority: rule.priority ?? index + 1
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(`Created resource ${newResource.resourceId}`);
|
||||
}
|
||||
|
||||
@@ -1097,6 +1293,399 @@ async function syncWhitelistUsers(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an inline resourcePolicy if one doesn't exist yet, and returns its ID.
|
||||
* Makes the policy visible to the admin role via rolePolicies.
|
||||
*/
|
||||
async function ensureInlinePolicy(
|
||||
existingPolicyId: number | null | undefined,
|
||||
orgId: string,
|
||||
resourceNiceId: string,
|
||||
adminRoleId: number,
|
||||
trx: Transaction
|
||||
): Promise<number> {
|
||||
if (existingPolicyId) {
|
||||
return existingPolicyId;
|
||||
}
|
||||
|
||||
const policyNiceId = await getUniqueResourcePolicyName(orgId);
|
||||
const [newPolicy] = await trx
|
||||
.insert(resourcePolicies)
|
||||
.values({
|
||||
niceId: policyNiceId,
|
||||
orgId,
|
||||
name: `default policy for ${resourceNiceId}`,
|
||||
sso: true,
|
||||
scope: "resource"
|
||||
})
|
||||
.returning();
|
||||
|
||||
await trx.insert(rolePolicies).values({
|
||||
roleId: adminRoleId,
|
||||
resourcePolicyId: newPolicy.resourcePolicyId
|
||||
});
|
||||
|
||||
return newPolicy.resourcePolicyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the inline policy's auth-related fields and policy-level tables
|
||||
* (used when no shared policy is specified in the blueprint).
|
||||
*/
|
||||
async function syncInlinePolicyAuth(
|
||||
policyId: number,
|
||||
orgId: string,
|
||||
resourceData: any,
|
||||
trx: Transaction
|
||||
) {
|
||||
// Update policy-level SSO/whitelist/applyRules fields
|
||||
await trx
|
||||
.update(resourcePolicies)
|
||||
.set({
|
||||
sso: resourceData.auth?.["sso-enabled"] ?? false,
|
||||
idpId: resourceData.auth?.["auto-login-idp"] || null,
|
||||
emailWhitelistEnabled: resourceData.auth?.["whitelist-users"]
|
||||
? resourceData.auth["whitelist-users"].length > 0
|
||||
: false,
|
||||
applyRules: !!(resourceData.rules && resourceData.rules.length > 0)
|
||||
})
|
||||
.where(eq(resourcePolicies.resourcePolicyId, policyId));
|
||||
|
||||
// Password
|
||||
await trx
|
||||
.delete(resourcePolicyPassword)
|
||||
.where(eq(resourcePolicyPassword.resourcePolicyId, policyId));
|
||||
if (resourceData.auth?.password) {
|
||||
const passwordHash = await hashPassword(resourceData.auth.password);
|
||||
await trx.insert(resourcePolicyPassword).values({
|
||||
resourcePolicyId: policyId,
|
||||
passwordHash
|
||||
});
|
||||
}
|
||||
|
||||
// Pincode
|
||||
await trx
|
||||
.delete(resourcePolicyPincode)
|
||||
.where(eq(resourcePolicyPincode.resourcePolicyId, policyId));
|
||||
if (resourceData.auth?.pincode) {
|
||||
const pincodeHash = await hashPassword(
|
||||
resourceData.auth.pincode.toString()
|
||||
);
|
||||
await trx.insert(resourcePolicyPincode).values({
|
||||
resourcePolicyId: policyId,
|
||||
pincodeHash,
|
||||
digitLength: 6
|
||||
});
|
||||
}
|
||||
|
||||
// Header auth
|
||||
await trx
|
||||
.delete(resourcePolicyHeaderAuth)
|
||||
.where(eq(resourcePolicyHeaderAuth.resourcePolicyId, policyId));
|
||||
if (resourceData.auth?.["basic-auth"]) {
|
||||
const headerAuthUser = resourceData.auth["basic-auth"]?.user;
|
||||
const headerAuthPassword = resourceData.auth["basic-auth"]?.password;
|
||||
const headerAuthExtendedCompatibility =
|
||||
resourceData.auth["basic-auth"]?.extendedCompatibility;
|
||||
if (
|
||||
headerAuthUser &&
|
||||
headerAuthPassword &&
|
||||
headerAuthExtendedCompatibility !== null
|
||||
) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(`${headerAuthUser}:${headerAuthPassword}`).toString(
|
||||
"base64"
|
||||
)
|
||||
);
|
||||
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||
resourcePolicyId: policyId,
|
||||
headerAuthHash,
|
||||
extendedCompatibility: headerAuthExtendedCompatibility
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// SSO roles → rolePolicies
|
||||
if (resourceData.auth?.["sso-roles"] !== undefined) {
|
||||
await syncRolePolicies(
|
||||
policyId,
|
||||
resourceData.auth["sso-roles"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
// SSO users → userPolicies
|
||||
if (resourceData.auth?.["sso-users"] !== undefined) {
|
||||
await syncUserPolicies(
|
||||
policyId,
|
||||
resourceData.auth["sso-users"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
// Whitelist → resourcePolicyWhiteList
|
||||
if (resourceData.auth?.["whitelist-users"] !== undefined) {
|
||||
await syncWhitelistPolicyUsers(
|
||||
policyId,
|
||||
resourceData.auth["whitelist-users"],
|
||||
orgId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs rules into the resourcePolicyRules table (inline policy mode).
|
||||
*/
|
||||
async function syncInlinePolicyRules(
|
||||
policyId: number,
|
||||
rules: any[],
|
||||
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) {
|
||||
if (
|
||||
existingRule.action !== getRuleAction(rule.action) ||
|
||||
existingRule.match !== rule.match.toUpperCase() ||
|
||||
existingRule.value !==
|
||||
getRuleValue(rule.match.toUpperCase(), rule.value) ||
|
||||
existingRule.priority !== intendedPriority
|
||||
) {
|
||||
validateRule(rule);
|
||||
await trx
|
||||
.update(resourcePolicyRules)
|
||||
.set({
|
||||
action: getRuleAction(rule.action) as
|
||||
| "ACCEPT"
|
||||
| "DROP"
|
||||
| "PASS",
|
||||
match: rule.match.toUpperCase() as
|
||||
| "CIDR"
|
||||
| "IP"
|
||||
| "PATH",
|
||||
value: getRuleValue(
|
||||
rule.match.toUpperCase(),
|
||||
rule.value
|
||||
),
|
||||
priority: intendedPriority
|
||||
})
|
||||
.where(eq(resourcePolicyRules.ruleId, existingRule.ruleId));
|
||||
}
|
||||
} else {
|
||||
validateRule(rule);
|
||||
await trx.insert(resourcePolicyRules).values({
|
||||
resourcePolicyId: policyId,
|
||||
action: getRuleAction(rule.action) as
|
||||
| "ACCEPT"
|
||||
| "DROP"
|
||||
| "PASS",
|
||||
match: rule.match.toUpperCase() as "CIDR" | "IP" | "PATH",
|
||||
value: getRuleValue(rule.match.toUpperCase(), rule.value),
|
||||
priority: intendedPriority
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs SSO roles to the rolePolicies table (inline policy mode).
|
||||
*/
|
||||
async function syncRolePolicies(
|
||||
policyId: number,
|
||||
ssoRoles: string[],
|
||||
orgId: string,
|
||||
trx: Transaction
|
||||
) {
|
||||
const existingRolePoliciesList = 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) {
|
||||
throw new Error(`Role not found: ${roleName} in org ${orgId}`);
|
||||
}
|
||||
|
||||
if (role.isAdmin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingRolePolicy = existingRolePoliciesList.find(
|
||||
(rp) => rp.roleId === role.roleId
|
||||
);
|
||||
|
||||
if (!existingRolePolicy) {
|
||||
await trx.insert(rolePolicies).values({
|
||||
roleId: role.roleId,
|
||||
resourcePolicyId: policyId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const existingRolePolicy of existingRolePoliciesList) {
|
||||
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.roleId, existingRolePolicy.roleId),
|
||||
eq(rolePolicies.resourcePolicyId, policyId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs SSO users to the userPolicies table (inline policy mode).
|
||||
*/
|
||||
async function syncUserPolicies(
|
||||
policyId: number,
|
||||
ssoUsers: string[],
|
||||
orgId: string,
|
||||
trx: Transaction
|
||||
) {
|
||||
const existingUserPoliciesList = 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) {
|
||||
throw new Error(`User not found: ${username} in org ${orgId}`);
|
||||
}
|
||||
|
||||
const existingUserPolicy = existingUserPoliciesList.find(
|
||||
(up) => up.userId === user.user.userId
|
||||
);
|
||||
|
||||
if (!existingUserPolicy) {
|
||||
await trx.insert(userPolicies).values({
|
||||
userId: user.user.userId,
|
||||
resourcePolicyId: policyId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const existingUserPolicy of existingUserPoliciesList) {
|
||||
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)
|
||||
) {
|
||||
await trx
|
||||
.delete(userPolicies)
|
||||
.where(
|
||||
and(
|
||||
eq(userPolicies.userId, existingUserPolicy.userId),
|
||||
eq(userPolicies.resourcePolicyId, policyId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs whitelist emails to the resourcePolicyWhiteList table (inline policy mode).
|
||||
*/
|
||||
async function syncWhitelistPolicyUsers(
|
||||
policyId: number,
|
||||
whitelistUsers: string[],
|
||||
orgId: string,
|
||||
trx: Transaction
|
||||
) {
|
||||
const existingWhitelist = await trx
|
||||
.select()
|
||||
.from(resourcePolicyWhiteList)
|
||||
.where(eq(resourcePolicyWhiteList.resourcePolicyId, policyId));
|
||||
|
||||
for (const email of whitelistUsers) {
|
||||
const existingEntry = existingWhitelist.find((w) => w.email === email);
|
||||
|
||||
if (!existingEntry) {
|
||||
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.whitelistId,
|
||||
existingEntry.whitelistId
|
||||
),
|
||||
eq(resourcePolicyWhiteList.resourcePolicyId, policyId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfHealthcheckChanged(
|
||||
existing: TargetHealthCheck | undefined,
|
||||
incoming: TargetHealthCheck | undefined
|
||||
|
||||
@@ -162,9 +162,10 @@ export const HeaderSchema = z.object({
|
||||
});
|
||||
|
||||
// Schema for individual resource
|
||||
export const ResourceSchema = z
|
||||
export const PublicResourceSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
policy: z.string().optional(),
|
||||
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
||||
ssl: z.boolean().optional(),
|
||||
scheme: z.enum(["http", "https"]).optional(),
|
||||
@@ -340,7 +341,8 @@ export const ResourceSchema = z
|
||||
if (parts.includes("*", 1)) return false; // no further wildcards
|
||||
if (parts.length < 3) return false; // need at least *.label.tld
|
||||
|
||||
const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
|
||||
const labelRegex =
|
||||
/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
|
||||
return parts.slice(1).every((label) => labelRegex.test(label));
|
||||
},
|
||||
{
|
||||
@@ -354,7 +356,7 @@ export function isTargetsOnlyResource(resource: any): boolean {
|
||||
return Object.keys(resource).length === 1 && resource.targets;
|
||||
}
|
||||
|
||||
export const ClientResourceSchema = z
|
||||
export const PrivateResourceSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255),
|
||||
mode: z.enum(["host", "cidr", "http"]),
|
||||
@@ -435,19 +437,19 @@ export const ClientResourceSchema = z
|
||||
export const ConfigSchema = z
|
||||
.object({
|
||||
"proxy-resources": z
|
||||
.record(z.string(), ResourceSchema)
|
||||
.record(z.string(), PublicResourceSchema)
|
||||
.optional()
|
||||
.prefault({}),
|
||||
"public-resources": z
|
||||
.record(z.string(), ResourceSchema)
|
||||
.record(z.string(), PublicResourceSchema)
|
||||
.optional()
|
||||
.prefault({}),
|
||||
"client-resources": z
|
||||
.record(z.string(), ClientResourceSchema)
|
||||
.record(z.string(), PrivateResourceSchema)
|
||||
.optional()
|
||||
.prefault({}),
|
||||
"private-resources": z
|
||||
.record(z.string(), ClientResourceSchema)
|
||||
.record(z.string(), PrivateResourceSchema)
|
||||
.optional()
|
||||
.prefault({}),
|
||||
sites: z.record(z.string(), SiteSchema).optional().prefault({})
|
||||
@@ -472,10 +474,13 @@ export const ConfigSchema = z
|
||||
}
|
||||
|
||||
return data as {
|
||||
"proxy-resources": Record<string, z.infer<typeof ResourceSchema>>;
|
||||
"proxy-resources": Record<
|
||||
string,
|
||||
z.infer<typeof PublicResourceSchema>
|
||||
>;
|
||||
"client-resources": Record<
|
||||
string,
|
||||
z.infer<typeof ClientResourceSchema>
|
||||
z.infer<typeof PrivateResourceSchema>
|
||||
>;
|
||||
sites: Record<string, z.infer<typeof SiteSchema>>;
|
||||
};
|
||||
@@ -614,5 +619,5 @@ export const ConfigSchema = z
|
||||
// Type inference from the schema
|
||||
export type Site = z.infer<typeof SiteSchema>;
|
||||
export type Target = z.infer<typeof TargetSchema>;
|
||||
export type Resource = z.infer<typeof ResourceSchema>;
|
||||
export type Resource = z.infer<typeof PublicResourceSchema>;
|
||||
export type Config = z.infer<typeof ConfigSchema>;
|
||||
|
||||
Reference in New Issue
Block a user