mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 18:27:36 +00:00
Add temporal permissions cache handling
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { MongoAbility, RawRuleOf } from "@casl/ability";
|
import { MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { MongoQuery } from "@ucast/mongo2js";
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
@@ -283,7 +284,7 @@ export type TPermissionServiceFactory = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
checkPermissions: ProjectPermissionSet;
|
checkPermissions: ProjectPermissionSet;
|
||||||
}) => Promise<boolean>;
|
}) => Promise<boolean>;
|
||||||
invalidateProjectPermissionCache: (projectId: string) => Promise<void>;
|
invalidateProjectPermissionCache: (projectId: string, tx?: Knex) => Promise<void>;
|
||||||
invalidateUserProjectPermissionCache: (userId: string) => Promise<void>;
|
invalidateUserProjectPermissionCache: (userId: string) => Promise<void>;
|
||||||
invalidateIdentityProjectPermissionCache: (identityId: string) => Promise<void>;
|
invalidateIdentityProjectPermissionCache: (identityId: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PackRule, unpackRules } from "@casl/ability/extra";
|
|||||||
import { requestContext } from "@fastify/request-context";
|
import { requestContext } from "@fastify/request-context";
|
||||||
import { MongoQuery } from "@ucast/mongo2js";
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActionProjectType,
|
ActionProjectType,
|
||||||
@@ -87,9 +88,13 @@ export const permissionServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const invalidateProjectPermissionCache = async (projectId: string) => {
|
const invalidateProjectPermissionCache = async (projectId: string, tx?: Knex) => {
|
||||||
const pattern = KeyStorePrefixes.ProjectPermissionPattern(projectId);
|
const projectPermissionDalVersionKey = KeyStorePrefixes.ProjectPermissionDalVersion(projectId);
|
||||||
await keyStore.deleteItems({ pattern });
|
await keyStore.pgIncrementBy(projectPermissionDalVersionKey, {
|
||||||
|
incr: 1,
|
||||||
|
tx,
|
||||||
|
expiry: KeyStoreTtls.ProjectPermissionDalVersionTtl
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const invalidateUserProjectPermissionCache = async (userId: string) => {
|
const invalidateUserProjectPermissionCache = async (userId: string) => {
|
||||||
@@ -102,6 +107,59 @@ export const permissionServiceFactory = ({
|
|||||||
await keyStore.deleteItems({ pattern });
|
await keyStore.deleteItems({ pattern });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const calculateProjectPermissionTtl = (membership: unknown): number => {
|
||||||
|
const now = new Date();
|
||||||
|
let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds;
|
||||||
|
|
||||||
|
const getMinEndTime = (items: Array<{ temporaryAccessEndTime?: Date | null; isTemporary?: boolean }>) => {
|
||||||
|
return items
|
||||||
|
.filter((item) => item.isTemporary && item.temporaryAccessEndTime)
|
||||||
|
.map((item) => item.temporaryAccessEndTime!)
|
||||||
|
.filter((endTime) => endTime > now)
|
||||||
|
.reduce((min, endTime) => (!min || endTime < min ? endTime : min), null as Date | null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const roleTimes: Date[] = [];
|
||||||
|
const additionalPrivilegeTimes: Date[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
membership &&
|
||||||
|
typeof membership === "object" &&
|
||||||
|
"roles" in membership &&
|
||||||
|
Array.isArray((membership as Record<string, unknown>).roles)
|
||||||
|
) {
|
||||||
|
const roles = (membership as Record<string, unknown>).roles as Array<{
|
||||||
|
temporaryAccessEndTime?: Date | null;
|
||||||
|
isTemporary?: boolean;
|
||||||
|
}>;
|
||||||
|
const minRoleEndTime = getMinEndTime(roles);
|
||||||
|
if (minRoleEndTime) roleTimes.push(minRoleEndTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
membership &&
|
||||||
|
typeof membership === "object" &&
|
||||||
|
"additionalPrivileges" in membership &&
|
||||||
|
Array.isArray((membership as Record<string, unknown>).additionalPrivileges)
|
||||||
|
) {
|
||||||
|
const additionalPrivileges = (membership as Record<string, unknown>).additionalPrivileges as Array<{
|
||||||
|
temporaryAccessEndTime?: Date | null;
|
||||||
|
isTemporary?: boolean;
|
||||||
|
}>;
|
||||||
|
const minAdditionalEndTime = getMinEndTime(additionalPrivileges);
|
||||||
|
if (minAdditionalEndTime) additionalPrivilegeTimes.push(minAdditionalEndTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allEndTimes = [...roleTimes, ...additionalPrivilegeTimes];
|
||||||
|
if (allEndTimes.length > 0) {
|
||||||
|
const nearestEndTime = allEndTimes.reduce((min, endTime) => (!min || endTime < min ? endTime : min));
|
||||||
|
const timeUntilExpiry = Math.floor((nearestEndTime.getTime() - now.getTime()) / 1000);
|
||||||
|
minTtl = Math.min(minTtl, Math.max(1, timeUntilExpiry));
|
||||||
|
}
|
||||||
|
|
||||||
|
return minTtl;
|
||||||
|
};
|
||||||
|
|
||||||
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||||
const rules = projectUserRoles
|
const rules = projectUserRoles
|
||||||
.map(({ role, permissions }) => {
|
.map(({ role, permissions }) => {
|
||||||
@@ -596,13 +654,6 @@ export const permissionServiceFactory = ({
|
|||||||
actorId = assumedPrivilegeDetailsCtx.actorId;
|
actorId = assumedPrivilegeDetailsCtx.actorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = KeyStorePrefixes.ProjectPermission(
|
|
||||||
projectId,
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actionProjectType || ActionProjectType.Any
|
|
||||||
);
|
|
||||||
|
|
||||||
if (actor === ActorType.SERVICE) {
|
if (actor === ActorType.SERVICE) {
|
||||||
return getServiceTokenProjectPermission({
|
return getServiceTokenProjectPermission({
|
||||||
serviceTokenId: actorId,
|
serviceTokenId: actorId,
|
||||||
@@ -612,6 +663,18 @@ export const permissionServiceFactory = ({
|
|||||||
}) as Promise<TProjectPermissionRT<T>>;
|
}) as Promise<TProjectPermissionRT<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedProjectPermissionVersion = await keyStore.pgGetIntItem(
|
||||||
|
KeyStorePrefixes.ProjectPermissionDalVersion(projectId)
|
||||||
|
);
|
||||||
|
const projectPermissionVersion = Number(cachedProjectPermissionVersion || 0);
|
||||||
|
const cacheKey = KeyStorePrefixes.ProjectPermission(
|
||||||
|
projectId,
|
||||||
|
projectPermissionVersion,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actionProjectType || ActionProjectType.Any
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cachedData = await keyStore.getItem(cacheKey);
|
const cachedData = await keyStore.getItem(cacheKey);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
@@ -622,7 +685,6 @@ export const permissionServiceFactory = ({
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
// Reconstruct the permission object from cached rules
|
|
||||||
const permission = createMongoAbility<ProjectPermissionSet>(parsed.rules, {
|
const permission = createMongoAbility<ProjectPermissionSet>(parsed.rules, {
|
||||||
conditionsMatcher
|
conditionsMatcher
|
||||||
});
|
});
|
||||||
@@ -673,11 +735,8 @@ export const permissionServiceFactory = ({
|
|||||||
membership: result.membership
|
membership: result.membership
|
||||||
};
|
};
|
||||||
|
|
||||||
await keyStore.setItemWithExpiry(
|
const ttl = calculateProjectPermissionTtl(result.membership);
|
||||||
cacheKey,
|
await keyStore.setItemWithExpiry(cacheKey, ttl, JSON.stringify(cacheData));
|
||||||
KeyStoreTtls.ProjectPermissionCacheInSeconds,
|
|
||||||
JSON.stringify(cacheData)
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error, "Failed to cache project permission");
|
logger.error(error, "Failed to cache project permission");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,11 +65,16 @@ export const KeyStorePrefixes = {
|
|||||||
ActiveSSEConnections: (projectId: string, identityId: string, connectionId: string) =>
|
ActiveSSEConnections: (projectId: string, identityId: string, connectionId: string) =>
|
||||||
`sse-connections:${projectId}:${identityId}:${connectionId}` as const,
|
`sse-connections:${projectId}:${identityId}:${connectionId}` as const,
|
||||||
|
|
||||||
ProjectPermission: (projectId: string, actorType: string, actorId: string, actionProjectType: string) =>
|
ProjectPermission: (
|
||||||
`project-permission:${projectId}:${actorType}:${actorId}:${actionProjectType}` as const,
|
projectId: string,
|
||||||
ProjectPermissionPattern: (projectId: string) => `project-permission:${projectId}:*` as const,
|
version: number,
|
||||||
UserProjectPermissionPattern: (userId: string) => `project-permission:*:USER:${userId}:*` as const,
|
actorType: string,
|
||||||
IdentityProjectPermissionPattern: (identityId: string) => `project-permission:*:IDENTITY:${identityId}:*` as const,
|
actorId: string,
|
||||||
|
actionProjectType: string
|
||||||
|
) => `project-permission:${projectId}:${version}:${actorType}:${actorId}:${actionProjectType}` as const,
|
||||||
|
ProjectPermissionDalVersion: (projectId: string) => `project-permission:${projectId}:dal-version` as const,
|
||||||
|
UserProjectPermissionPattern: (userId: string) => `project-permission:*:*:USER:${userId}:*` as const,
|
||||||
|
IdentityProjectPermissionPattern: (identityId: string) => `project-permission:*:*:IDENTITY:${identityId}:*` as const,
|
||||||
GroupMemberProjectPermissionPattern: (projectId: string, groupId: string) =>
|
GroupMemberProjectPermissionPattern: (projectId: string, groupId: string) =>
|
||||||
`group-member-project-permission:${projectId}:${groupId}:*` as const
|
`group-member-project-permission:${projectId}:${groupId}:*` as const
|
||||||
};
|
};
|
||||||
@@ -78,7 +83,8 @@ export const KeyStoreTtls = {
|
|||||||
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
|
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
|
||||||
SetSecretSyncLastRunTimestampInSeconds: 60,
|
SetSecretSyncLastRunTimestampInSeconds: 60,
|
||||||
AccessTokenStatusUpdateInSeconds: 120,
|
AccessTokenStatusUpdateInSeconds: 120,
|
||||||
ProjectPermissionCacheInSeconds: 300 // 5 minutes
|
ProjectPermissionCacheInSeconds: 300, // 5 minutes
|
||||||
|
ProjectPermissionDalVersionTtl: "15m" // Project permission DAL version TTL
|
||||||
};
|
};
|
||||||
|
|
||||||
type TDeleteItems = {
|
type TDeleteItems = {
|
||||||
|
|||||||
Reference in New Issue
Block a user