Adjust verify session queries to use policies

This commit is contained in:
Owen
2026-05-04 17:30:10 -07:00
parent 7b05c02508
commit 6cab223f12
2 changed files with 220 additions and 61 deletions

View File

@@ -17,10 +17,13 @@ import {
resourceHeaderAuth, resourceHeaderAuth,
ResourceHeaderAuth, ResourceHeaderAuth,
resourceRules, resourceRules,
resourcePolicyRules,
resources, resources,
roleResources, roleResources,
rolePolicies,
sessions, sessions,
userResources, userResources,
userPolicies,
users, users,
ResourceHeaderAuthExtendedCompatibility, ResourceHeaderAuthExtendedCompatibility,
resourceHeaderAuthExtendedCompatibility resourceHeaderAuthExtendedCompatibility
@@ -154,58 +157,126 @@ export async function getRoleName(roleId: number): Promise<string | null> {
} }
/** /**
* Check if role has access to resource * Check if role has access to resource (direct or via resource policy)
*/ */
export async function getRoleResourceAccess( export async function getRoleResourceAccess(
resourceId: number, resourceId: number,
roleIds: number[] roleIds: number[]
) { ) {
const roleResourceAccess = await db const [direct, viaPolicies] = await Promise.all([
.select() db
.from(roleResources) .select()
.where( .from(roleResources)
and( .where(
eq(roleResources.resourceId, resourceId), and(
inArray(roleResources.roleId, roleIds) eq(roleResources.resourceId, resourceId),
inArray(roleResources.roleId, roleIds)
)
),
db
.select({
roleId: rolePolicies.roleId,
resourcePolicyId: rolePolicies.resourcePolicyId
})
.from(rolePolicies)
.innerJoin(
resources,
eq(resources.resourcePolicyId, rolePolicies.resourcePolicyId)
) )
); .where(
and(
eq(resources.resourceId, resourceId),
inArray(rolePolicies.roleId, roleIds)
)
)
]);
return roleResourceAccess.length > 0 ? roleResourceAccess : null; const combined = [...direct, ...viaPolicies];
return combined.length > 0 ? combined : null;
} }
/** /**
* Check if user has direct access to resource * Check if user has access to resource (direct or via resource policy)
*/ */
export async function getUserResourceAccess( export async function getUserResourceAccess(
userId: string, userId: string,
resourceId: number resourceId: number
) { ) {
const userResourceAccess = await db const [direct, viaPolicies] = await Promise.all([
.select() db
.from(userResources) .select()
.where( .from(userResources)
and( .where(
eq(userResources.userId, userId), and(
eq(userResources.resourceId, resourceId) eq(userResources.userId, userId),
eq(userResources.resourceId, resourceId)
)
) )
) .limit(1),
.limit(1); db
.select({
userId: userPolicies.userId,
resourcePolicyId: userPolicies.resourcePolicyId
})
.from(userPolicies)
.innerJoin(
resources,
eq(resources.resourcePolicyId, userPolicies.resourcePolicyId)
)
.where(
and(
eq(resources.resourceId, resourceId),
eq(userPolicies.userId, userId)
)
)
.limit(1)
]);
return userResourceAccess.length > 0 ? userResourceAccess[0] : null; return direct[0] ?? viaPolicies[0] ?? null;
} }
/** /**
* Get resource rules for a given resource * Get resource rules for a given resource (direct and via resource policy)
*/ */
export async function getResourceRules( export async function getResourceRules(
resourceId: number resourceId: number
): Promise<ResourceRule[]> { ): Promise<ResourceRule[]> {
const rules = await db const [directRules, policyRules] = await Promise.all([
.select() db
.from(resourceRules) .select()
.where(eq(resourceRules.resourceId, resourceId)); .from(resourceRules)
.where(eq(resourceRules.resourceId, resourceId)),
db
.select({
ruleId: resourcePolicyRules.ruleId,
resourceId: sql<number>`${resourceId}`,
enabled: resourcePolicyRules.enabled,
priority: resourcePolicyRules.priority,
action: resourcePolicyRules.action,
match: resourcePolicyRules.match,
value: resourcePolicyRules.value
})
.from(resourcePolicyRules)
.innerJoin(
resources,
eq(
resources.resourcePolicyId,
resourcePolicyRules.resourcePolicyId
)
)
.where(eq(resources.resourceId, resourceId))
]);
return rules; const maxDirectPriority = directRules.reduce(
(max, r) => Math.max(max, r.priority),
0
);
const offsetPolicyRules = policyRules.map((r) => ({
...r,
priority: maxDirectPriority + r.priority
}));
return [...directRules, ...offsetPolicyRules] as ResourceRule[];
} }
/** /**

View File

@@ -45,8 +45,11 @@ import {
users, users,
userOrgs, userOrgs,
roleResources, roleResources,
rolePolicies,
userResources, userResources,
userPolicies,
resourceRules, resourceRules,
resourcePolicyRules,
userOrgRoles, userOrgRoles,
roles roles
} from "@server/db"; } from "@server/db";
@@ -430,7 +433,10 @@ hybridRouter.get(
); );
// Decrypt and save key file // Decrypt and save key file
const decryptedKey = decrypt(cert.keyFile!, config.getRawConfig().server.secret!); const decryptedKey = decrypt(
cert.keyFile!,
config.getRawConfig().server.secret!
);
// Return only the certificate data without org information // Return only the certificate data without org information
return { return {
@@ -531,7 +537,10 @@ hybridRouter.get(
wildcardCandidates.length > 0 wildcardCandidates.length > 0
? and( ? and(
eq(resources.wildcard, true), eq(resources.wildcard, true),
inArray(resources.fullDomain, wildcardCandidates) inArray(
resources.fullDomain,
wildcardCandidates
)
) )
: sql`false` : sql`false`
) )
@@ -545,10 +554,10 @@ hybridRouter.get(
if ( if (
result && result &&
await checkExitNodeOrg( (await checkExitNodeOrg(
remoteExitNode.exitNodeId, remoteExitNode.exitNodeId,
result.resources.orgId result.resources.orgId
) ))
) { ) {
// If the exit node is not allowed for the org, return an error // If the exit node is not allowed for the org, return an error
return next( return next(
@@ -1132,22 +1141,43 @@ hybridRouter.get(
); );
} }
const roleResourceAccess = await db const [direct, viaPolicies] = await Promise.all([
.select() db
.from(roleResources) .select()
.where( .from(roleResources)
and( .where(
eq(roleResources.resourceId, resourceId), and(
eq(roleResources.roleId, roleId) eq(roleResources.resourceId, resourceId),
eq(roleResources.roleId, roleId)
)
) )
) .limit(1),
.limit(1); db
.select({
roleId: rolePolicies.roleId,
resourcePolicyId: rolePolicies.resourcePolicyId
})
.from(rolePolicies)
.innerJoin(
resources,
eq(
resources.resourcePolicyId,
rolePolicies.resourcePolicyId
)
)
.where(
and(
eq(resources.resourceId, resourceId),
eq(rolePolicies.roleId, roleId)
)
)
.limit(1)
]);
const result = const result = direct[0] ?? viaPolicies[0] ?? null;
roleResourceAccess.length > 0 ? roleResourceAccess[0] : null;
return response<typeof roleResources.$inferSelect | null>(res, { return response<typeof roleResources.$inferSelect | null>(res, {
data: result, data: result as any,
success: true, success: true,
error: false, error: false,
message: result message: result
@@ -1222,21 +1252,44 @@ hybridRouter.get(
); );
} }
const roleResourceAccess = await db const [direct, viaPolicies] = await Promise.all([
.select({ db
resourceId: roleResources.resourceId, .select({
roleId: roleResources.roleId resourceId: roleResources.resourceId,
}) roleId: roleResources.roleId
.from(roleResources) })
.where( .from(roleResources)
and( .where(
eq(roleResources.resourceId, resourceId), and(
inArray(roleResources.roleId, roleIds) eq(roleResources.resourceId, resourceId),
) inArray(roleResources.roleId, roleIds)
); )
),
roleIds.length > 0
? db
.select({
resourceId: sql<number>`${resourceId}`,
roleId: rolePolicies.roleId
})
.from(rolePolicies)
.innerJoin(
resources,
eq(
resources.resourcePolicyId,
rolePolicies.resourcePolicyId
)
)
.where(
and(
eq(resources.resourceId, resourceId),
inArray(rolePolicies.roleId, roleIds)
)
)
: Promise.resolve([])
]);
const result = const combined = [...direct, ...viaPolicies];
roleResourceAccess.length > 0 ? roleResourceAccess : null; const result = combined.length > 0 ? combined : null;
return response<{ resourceId: number; roleId: number }[] | null>( return response<{ resourceId: number; roleId: number }[] | null>(
res, res,
@@ -1397,10 +1450,45 @@ hybridRouter.get(
); );
} }
const rules = await db const [directRules, policyRules] = await Promise.all([
.select() db
.from(resourceRules) .select()
.where(eq(resourceRules.resourceId, resourceId)); .from(resourceRules)
.where(eq(resourceRules.resourceId, resourceId)),
db
.select({
ruleId: resourcePolicyRules.ruleId,
resourceId: sql<number>`${resourceId}`,
enabled: resourcePolicyRules.enabled,
priority: resourcePolicyRules.priority,
action: resourcePolicyRules.action,
match: resourcePolicyRules.match,
value: resourcePolicyRules.value
})
.from(resourcePolicyRules)
.innerJoin(
resources,
eq(
resources.resourcePolicyId,
resourcePolicyRules.resourcePolicyId
)
)
.where(eq(resources.resourceId, resourceId))
]);
const maxDirectPriority = directRules.reduce(
(max, r) => Math.max(max, r.priority),
0
);
const offsetPolicyRules = policyRules.map((r) => ({
...r,
priority: maxDirectPriority + r.priority
}));
const rules = [
...directRules,
...offsetPolicyRules
] as (typeof resourceRules.$inferSelect)[];
// backward compatibility: COUNTRY -> GEOIP // backward compatibility: COUNTRY -> GEOIP
// TODO: remove this after a few versions once all exit nodes are updated // TODO: remove this after a few versions once all exit nodes are updated