diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 46b45b1a0..17844e13c 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -17,10 +17,13 @@ import { resourceHeaderAuth, ResourceHeaderAuth, resourceRules, + resourcePolicyRules, resources, roleResources, + rolePolicies, sessions, userResources, + userPolicies, users, ResourceHeaderAuthExtendedCompatibility, resourceHeaderAuthExtendedCompatibility @@ -154,58 +157,126 @@ export async function getRoleName(roleId: number): Promise { } /** - * Check if role has access to resource + * Check if role has access to resource (direct or via resource policy) */ export async function getRoleResourceAccess( resourceId: number, roleIds: number[] ) { - const roleResourceAccess = await db - .select() - .from(roleResources) - .where( - and( - eq(roleResources.resourceId, resourceId), - inArray(roleResources.roleId, roleIds) + const [direct, viaPolicies] = await Promise.all([ + db + .select() + .from(roleResources) + .where( + and( + 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( userId: string, resourceId: number ) { - const userResourceAccess = await db - .select() - .from(userResources) - .where( - and( - eq(userResources.userId, userId), - eq(userResources.resourceId, resourceId) + const [direct, viaPolicies] = await Promise.all([ + db + .select() + .from(userResources) + .where( + and( + 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( resourceId: number ): Promise { - const rules = await db - .select() - .from(resourceRules) - .where(eq(resourceRules.resourceId, resourceId)); + const [directRules, policyRules] = await Promise.all([ + db + .select() + .from(resourceRules) + .where(eq(resourceRules.resourceId, resourceId)), + db + .select({ + ruleId: resourcePolicyRules.ruleId, + resourceId: sql`${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[]; } /** diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index 98e7ff671..508952341 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -45,8 +45,11 @@ import { users, userOrgs, roleResources, + rolePolicies, userResources, + userPolicies, resourceRules, + resourcePolicyRules, userOrgRoles, roles } from "@server/db"; @@ -430,7 +433,10 @@ hybridRouter.get( ); // 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 { @@ -531,7 +537,10 @@ hybridRouter.get( wildcardCandidates.length > 0 ? and( eq(resources.wildcard, true), - inArray(resources.fullDomain, wildcardCandidates) + inArray( + resources.fullDomain, + wildcardCandidates + ) ) : sql`false` ) @@ -545,10 +554,10 @@ hybridRouter.get( if ( result && - await checkExitNodeOrg( + (await checkExitNodeOrg( remoteExitNode.exitNodeId, result.resources.orgId - ) + )) ) { // If the exit node is not allowed for the org, return an error return next( @@ -1132,22 +1141,43 @@ hybridRouter.get( ); } - const roleResourceAccess = await db - .select() - .from(roleResources) - .where( - and( - eq(roleResources.resourceId, resourceId), - eq(roleResources.roleId, roleId) + const [direct, viaPolicies] = await Promise.all([ + db + .select() + .from(roleResources) + .where( + and( + 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 = - roleResourceAccess.length > 0 ? roleResourceAccess[0] : null; + const result = direct[0] ?? viaPolicies[0] ?? null; return response(res, { - data: result, + data: result as any, success: true, error: false, message: result @@ -1222,21 +1252,44 @@ hybridRouter.get( ); } - const roleResourceAccess = await db - .select({ - resourceId: roleResources.resourceId, - roleId: roleResources.roleId - }) - .from(roleResources) - .where( - and( - eq(roleResources.resourceId, resourceId), - inArray(roleResources.roleId, roleIds) - ) - ); + const [direct, viaPolicies] = await Promise.all([ + db + .select({ + resourceId: roleResources.resourceId, + roleId: roleResources.roleId + }) + .from(roleResources) + .where( + and( + eq(roleResources.resourceId, resourceId), + inArray(roleResources.roleId, roleIds) + ) + ), + roleIds.length > 0 + ? db + .select({ + resourceId: sql`${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 = - roleResourceAccess.length > 0 ? roleResourceAccess : null; + const combined = [...direct, ...viaPolicies]; + const result = combined.length > 0 ? combined : null; return response<{ resourceId: number; roleId: number }[] | null>( res, @@ -1397,10 +1450,45 @@ hybridRouter.get( ); } - const rules = await db - .select() - .from(resourceRules) - .where(eq(resourceRules.resourceId, resourceId)); + const [directRules, policyRules] = await Promise.all([ + db + .select() + .from(resourceRules) + .where(eq(resourceRules.resourceId, resourceId)), + db + .select({ + ruleId: resourcePolicyRules.ruleId, + resourceId: sql`${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 // TODO: remove this after a few versions once all exit nodes are updated