mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 19:28:09 +00:00
Merge pull request #3528 from Infisical/ENG-2647
feat(admin): Invalidate Cache
This commit is contained in:
@@ -1,4 +1,8 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { applyJitter } from "@app/lib/dates";
|
||||||
|
import { delay as delayMs } from "@app/lib/delay";
|
||||||
import { Lock } from "@app/lib/red-lock";
|
import { Lock } from "@app/lib/red-lock";
|
||||||
|
|
||||||
export const mockKeyStore = (): TKeyStoreFactory => {
|
export const mockKeyStore = (): TKeyStoreFactory => {
|
||||||
@@ -18,6 +22,27 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
|||||||
delete store[key];
|
delete store[key];
|
||||||
return 1;
|
return 1;
|
||||||
},
|
},
|
||||||
|
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
|
||||||
|
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
||||||
|
let totalDeleted = 0;
|
||||||
|
const keys = Object.keys(store);
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i += batchSize) {
|
||||||
|
const batch = keys.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
for (const key of batch) {
|
||||||
|
if (regex.test(key)) {
|
||||||
|
delete store[key];
|
||||||
|
totalDeleted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await delayMs(Math.max(0, applyJitter(delay, jitter)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalDeleted;
|
||||||
|
},
|
||||||
getItem: async (key) => {
|
getItem: async (key) => {
|
||||||
const value = store[key];
|
const value = store[key];
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
|
|
||||||
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
||||||
|
import { applyJitter } from "@app/lib/dates";
|
||||||
|
import { delay as delayMs } from "@app/lib/delay";
|
||||||
import { Redlock, Settings } from "@app/lib/red-lock";
|
import { Redlock, Settings } from "@app/lib/red-lock";
|
||||||
|
|
||||||
export const PgSqlLock = {
|
export const PgSqlLock = {
|
||||||
@@ -48,6 +50,13 @@ export const KeyStoreTtls = {
|
|||||||
AccessTokenStatusUpdateInSeconds: 120
|
AccessTokenStatusUpdateInSeconds: 120
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TDeleteItems = {
|
||||||
|
pattern: string;
|
||||||
|
batchSize?: number;
|
||||||
|
delay?: number;
|
||||||
|
jitter?: number;
|
||||||
|
};
|
||||||
|
|
||||||
type TWaitTillReady = {
|
type TWaitTillReady = {
|
||||||
key: string;
|
key: string;
|
||||||
waitingCb?: () => void;
|
waitingCb?: () => void;
|
||||||
@@ -75,6 +84,35 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
|
|
||||||
const deleteItem = async (key: string) => redis.del(key);
|
const deleteItem = async (key: string) => redis.del(key);
|
||||||
|
|
||||||
|
const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => {
|
||||||
|
let cursor = "0";
|
||||||
|
let totalDeleted = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Await in loop is needed so that Redis is not overwhelmed
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads
|
||||||
|
cursor = nextCursor;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i += batchSize) {
|
||||||
|
const batch = keys.slice(i, i + batchSize);
|
||||||
|
const pipeline = redis.pipeline();
|
||||||
|
for (const key of batch) {
|
||||||
|
pipeline.unlink(key);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await pipeline.exec();
|
||||||
|
totalDeleted += batch.length;
|
||||||
|
console.log("BATCH DONE");
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await delayMs(Math.max(0, applyJitter(delay, jitter)));
|
||||||
|
}
|
||||||
|
} while (cursor !== "0");
|
||||||
|
|
||||||
|
return totalDeleted;
|
||||||
|
};
|
||||||
|
|
||||||
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
||||||
|
|
||||||
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
|
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
|
||||||
@@ -94,7 +132,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
waitingCb?.();
|
waitingCb?.();
|
||||||
setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
|
setTimeout(resolve, Math.max(0, applyJitter(delay, jitter)));
|
||||||
});
|
});
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -108,6 +146,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
setExpiry,
|
setExpiry,
|
||||||
setItemWithExpiry,
|
setItemWithExpiry,
|
||||||
deleteItem,
|
deleteItem,
|
||||||
|
deleteItems,
|
||||||
incrementBy,
|
incrementBy,
|
||||||
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
||||||
return redisLock.acquire(resources, duration, settings);
|
return redisLock.acquire(resources, duration, settings);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
|
import { applyJitter } from "@app/lib/dates";
|
||||||
|
import { delay as delayMs } from "@app/lib/delay";
|
||||||
import { Lock } from "@app/lib/red-lock";
|
import { Lock } from "@app/lib/red-lock";
|
||||||
|
|
||||||
import { TKeyStoreFactory } from "./keystore";
|
import { TKeyStoreFactory } from "./keystore";
|
||||||
@@ -19,6 +23,27 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
|
|||||||
delete store[key];
|
delete store[key];
|
||||||
return 1;
|
return 1;
|
||||||
},
|
},
|
||||||
|
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
|
||||||
|
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
||||||
|
let totalDeleted = 0;
|
||||||
|
const keys = Object.keys(store);
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i += batchSize) {
|
||||||
|
const batch = keys.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
for (const key of batch) {
|
||||||
|
if (regex.test(key)) {
|
||||||
|
delete store[key];
|
||||||
|
totalDeleted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await delayMs(Math.max(0, applyJitter(delay, jitter)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalDeleted;
|
||||||
|
},
|
||||||
getItem: async (key) => {
|
getItem: async (key) => {
|
||||||
const value = store[key];
|
const value = store[key];
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const delay = (ms: number) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
TQueueSecretSyncSyncSecretsByIdDTO,
|
TQueueSecretSyncSyncSecretsByIdDTO,
|
||||||
TQueueSendSecretSyncActionFailedNotificationsDTO
|
TQueueSendSecretSyncActionFailedNotificationsDTO
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
import { CacheType } from "@app/services/super-admin/super-admin-types";
|
||||||
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
|
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
|
||||||
|
|
||||||
export enum QueueName {
|
export enum QueueName {
|
||||||
@@ -49,7 +50,8 @@ export enum QueueName {
|
|||||||
AccessTokenStatusUpdate = "access-token-status-update",
|
AccessTokenStatusUpdate = "access-token-status-update",
|
||||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||||
AppConnectionSecretSync = "app-connection-secret-sync",
|
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||||
SecretRotationV2 = "secret-rotation-v2"
|
SecretRotationV2 = "secret-rotation-v2",
|
||||||
|
InvalidateCache = "invalidate-cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@@ -81,7 +83,8 @@ export enum QueueJobs {
|
|||||||
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
||||||
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification"
|
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||||
|
InvalidateCache = "invalidate-cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TQueueJobTypes = {
|
export type TQueueJobTypes = {
|
||||||
@@ -234,6 +237,14 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.SecretRotationV2SendNotification;
|
name: QueueJobs.SecretRotationV2SendNotification;
|
||||||
payload: TSecretRotationSendNotificationJobPayload;
|
payload: TSecretRotationSendNotificationJobPayload;
|
||||||
};
|
};
|
||||||
|
[QueueName.InvalidateCache]: {
|
||||||
|
name: QueueJobs.InvalidateCache;
|
||||||
|
payload: {
|
||||||
|
data: {
|
||||||
|
type: CacheType;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||||
|
|||||||
@@ -100,3 +100,10 @@ export const publicSshCaLimit: RateLimitOptions = {
|
|||||||
max: 30, // conservative default
|
max: 30, // conservative default
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const invalidateCacheLimit: RateLimitOptions = {
|
||||||
|
timeWindow: 60 * 1000,
|
||||||
|
hook: "preValidation",
|
||||||
|
max: 1,
|
||||||
|
keyGenerator: (req) => req.realIp
|
||||||
|
};
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-
|
|||||||
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
||||||
import { slackServiceFactory } from "@app/services/slack/slack-service";
|
import { slackServiceFactory } from "@app/services/slack/slack-service";
|
||||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
import { invalidateCacheQueueFactory } from "@app/services/super-admin/invalidate-cache-queue";
|
||||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
|
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
|
||||||
@@ -611,6 +612,11 @@ export const registerRoutes = async (
|
|||||||
queueService
|
queueService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const invalidateCacheQueue = invalidateCacheQueueFactory({
|
||||||
|
keyStore,
|
||||||
|
queueService
|
||||||
|
});
|
||||||
|
|
||||||
const userService = userServiceFactory({
|
const userService = userServiceFactory({
|
||||||
userDAL,
|
userDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
@@ -722,7 +728,8 @@ export const registerRoutes = async (
|
|||||||
keyStore,
|
keyStore,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
microsoftTeamsService
|
microsoftTeamsService,
|
||||||
|
invalidateCacheQueue
|
||||||
});
|
});
|
||||||
|
|
||||||
const orgAdminService = orgAdminServiceFactory({
|
const orgAdminService = orgAdminServiceFactory({
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { z } from "zod";
|
|||||||
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
import { CacheType, LoginMethod } from "@app/services/super-admin/super-admin-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -548,4 +549,69 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/invalidate-cache",
|
||||||
|
config: {
|
||||||
|
rateLimit: invalidateCacheLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
type: z.nativeEnum(CacheType)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.superAdmin.invalidateCache(req.body.type);
|
||||||
|
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.InvalidateCache,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Cache invalidation job started"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/invalidating-cache-status",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
invalidating: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: (req, res, done) => {
|
||||||
|
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
|
||||||
|
verifySuperAdmin(req, res, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async () => {
|
||||||
|
const invalidating = await server.services.superAdmin.checkIfInvalidatingCache();
|
||||||
|
|
||||||
|
return {
|
||||||
|
invalidating
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
|
import { CacheType } from "./super-admin-types";
|
||||||
|
|
||||||
|
export type TInvalidateCacheQueueFactoryDep = {
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "deleteItems" | "setItemWithExpiry" | "deleteItem">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TInvalidateCacheQueueFactory = ReturnType<typeof invalidateCacheQueueFactory>;
|
||||||
|
|
||||||
|
export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalidateCacheQueueFactoryDep) => {
|
||||||
|
const startInvalidate = async (dto: {
|
||||||
|
data: {
|
||||||
|
type: CacheType;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
await queueService.queue(QueueName.InvalidateCache, QueueJobs.InvalidateCache, dto, {
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
jobId: `invalidate-cache-${dto.data.type}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
queueService.start(QueueName.InvalidateCache, async (job) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { type }
|
||||||
|
} = job.data;
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry("invalidating-cache", 1800, "true"); // 30 minutes max (in case the job somehow silently fails)
|
||||||
|
|
||||||
|
if (type === CacheType.ALL || type === CacheType.SECRETS)
|
||||||
|
await keyStore.deleteItems({ pattern: "secret-manager:*" });
|
||||||
|
|
||||||
|
await keyStore.deleteItem("invalidating-cache");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "Failed to invalidate cache");
|
||||||
|
await keyStore.deleteItem("invalidating-cache");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
startInvalidate
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -25,8 +25,10 @@ import { TOrgServiceFactory } from "../org/org-service";
|
|||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
||||||
import { UserAliasType } from "../user-alias/user-alias-types";
|
import { UserAliasType } from "../user-alias/user-alias-types";
|
||||||
|
import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue";
|
||||||
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
||||||
import {
|
import {
|
||||||
|
CacheType,
|
||||||
LoginMethod,
|
LoginMethod,
|
||||||
TAdminBootstrapInstanceDTO,
|
TAdminBootstrapInstanceDTO,
|
||||||
TAdminGetIdentitiesDTO,
|
TAdminGetIdentitiesDTO,
|
||||||
@@ -46,9 +48,10 @@ type TSuperAdminServiceFactoryDep = {
|
|||||||
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
|
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
|
||||||
kmsRootConfigDAL: TKmsRootConfigDALFactory;
|
kmsRootConfigDAL: TKmsRootConfigDALFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem" | "deleteItems">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
|
||||||
|
invalidateCacheQueue: TInvalidateCacheQueueFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
||||||
@@ -64,7 +67,7 @@ export let getServerCfg: () => Promise<
|
|||||||
|
|
||||||
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
|
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
|
||||||
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
|
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
|
||||||
const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
export const superAdminServiceFactory = ({
|
export const superAdminServiceFactory = ({
|
||||||
serverCfgDAL,
|
serverCfgDAL,
|
||||||
@@ -80,7 +83,8 @@ export const superAdminServiceFactory = ({
|
|||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
identityTokenAuthDAL,
|
identityTokenAuthDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
microsoftTeamsService
|
microsoftTeamsService,
|
||||||
|
invalidateCacheQueue
|
||||||
}: TSuperAdminServiceFactoryDep) => {
|
}: TSuperAdminServiceFactoryDep) => {
|
||||||
const initServerCfg = async () => {
|
const initServerCfg = async () => {
|
||||||
// TODO(akhilmhdh): bad pattern time less change this later to me itself
|
// TODO(akhilmhdh): bad pattern time less change this later to me itself
|
||||||
@@ -631,6 +635,16 @@ export const superAdminServiceFactory = ({
|
|||||||
await kmsService.updateEncryptionStrategy(strategy);
|
await kmsService.updateEncryptionStrategy(strategy);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const invalidateCache = async (type: CacheType) => {
|
||||||
|
await invalidateCacheQueue.startInvalidate({
|
||||||
|
data: { type }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIfInvalidatingCache = async () => {
|
||||||
|
return (await keyStore.getItem("invalidating-cache")) !== null;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initServerCfg,
|
initServerCfg,
|
||||||
updateServerCfg,
|
updateServerCfg,
|
||||||
@@ -644,6 +658,8 @@ export const superAdminServiceFactory = ({
|
|||||||
getConfiguredEncryptionStrategies,
|
getConfiguredEncryptionStrategies,
|
||||||
grantServerAdminAccessToUser,
|
grantServerAdminAccessToUser,
|
||||||
deleteIdentitySuperAdminAccess,
|
deleteIdentitySuperAdminAccess,
|
||||||
deleteUserSuperAdminAccess
|
deleteUserSuperAdminAccess,
|
||||||
|
invalidateCache,
|
||||||
|
checkIfInvalidatingCache
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,3 +44,8 @@ export enum LoginMethod {
|
|||||||
LDAP = "ldap",
|
LDAP = "ldap",
|
||||||
OIDC = "oidc"
|
OIDC = "oidc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CacheType {
|
||||||
|
ALL = "all",
|
||||||
|
SECRETS = "secrets"
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export enum PostHogEventTypes {
|
|||||||
IssueSshHostUserCert = "Issue SSH Host User Certificate",
|
IssueSshHostUserCert = "Issue SSH Host User Certificate",
|
||||||
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
||||||
SignCert = "Sign PKI Certificate",
|
SignCert = "Sign PKI Certificate",
|
||||||
IssueCert = "Issue PKI Certificate"
|
IssueCert = "Issue PKI Certificate",
|
||||||
|
InvalidateCache = "Invalidate Cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TSecretModifiedEvent = {
|
export type TSecretModifiedEvent = {
|
||||||
@@ -203,6 +204,13 @@ export type TIssueCertificateEvent = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TInvalidateCacheEvent = {
|
||||||
|
event: PostHogEventTypes.InvalidateCache;
|
||||||
|
properties: {
|
||||||
|
userAgent?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type TPostHogEvent = { distinctId: string } & (
|
export type TPostHogEvent = { distinctId: string } & (
|
||||||
| TSecretModifiedEvent
|
| TSecretModifiedEvent
|
||||||
| TAdminInitEvent
|
| TAdminInitEvent
|
||||||
@@ -221,4 +229,5 @@ export type TPostHogEvent = { distinctId: string } & (
|
|||||||
| TIssueSshHostHostCertEvent
|
| TIssueSshHostHostCertEvent
|
||||||
| TSignCertificateEvent
|
| TSignCertificateEvent
|
||||||
| TIssueCertificateEvent
|
| TIssueCertificateEvent
|
||||||
|
| TInvalidateCacheEvent
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export {
|
|||||||
useAdminGrantServerAdminAccess,
|
useAdminGrantServerAdminAccess,
|
||||||
useAdminRemoveIdentitySuperAdminAccess,
|
useAdminRemoveIdentitySuperAdminAccess,
|
||||||
useCreateAdminUser,
|
useCreateAdminUser,
|
||||||
|
useInvalidateCache,
|
||||||
useRemoveUserServerAdminAccess,
|
useRemoveUserServerAdminAccess,
|
||||||
useUpdateServerConfig,
|
useUpdateServerConfig,
|
||||||
useUpdateServerEncryptionStrategy
|
useUpdateServerEncryptionStrategy
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
|||||||
import {
|
import {
|
||||||
RootKeyEncryptionStrategy,
|
RootKeyEncryptionStrategy,
|
||||||
TCreateAdminUserDTO,
|
TCreateAdminUserDTO,
|
||||||
|
TInvalidateCacheDTO,
|
||||||
TServerConfig,
|
TServerConfig,
|
||||||
TUpdateServerConfigDTO
|
TUpdateServerConfigDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -126,3 +127,15 @@ export const useUpdateServerEncryptionStrategy = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useInvalidateCache = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<void, object, TInvalidateCacheDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
await apiRequest.post("/api/v1/admin/invalidate-cache", dto);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInvalidateCache() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
AdminGetIdentitiesFilters,
|
AdminGetIdentitiesFilters,
|
||||||
AdminGetUsersFilters,
|
AdminGetUsersFilters,
|
||||||
AdminIntegrationsConfig,
|
AdminIntegrationsConfig,
|
||||||
|
TGetInvalidatingCacheStatus,
|
||||||
TGetServerRootKmsEncryptionDetails,
|
TGetServerRootKmsEncryptionDetails,
|
||||||
TServerConfig
|
TServerConfig
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -22,8 +23,10 @@ export const adminQueryKeys = {
|
|||||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
||||||
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
||||||
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
||||||
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
|
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
||||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
|
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
|
||||||
|
getInvalidateCache: () => ["admin-invalidate-cache"] as const,
|
||||||
|
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchServerConfig = async () => {
|
export const fetchServerConfig = async () => {
|
||||||
@@ -118,3 +121,18 @@ export const useGetServerRootKmsEncryptionDetails = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetInvalidatingCacheStatus = (enabled = true) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: adminQueryKeys.getInvalidateCache(),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TGetInvalidatingCacheStatus>(
|
||||||
|
"/api/v1/admin/invalidating-cache-status"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.invalidating;
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
refetchInterval: (data) => (data ? 3000 : false)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type TServerConfig = {
|
|||||||
enabledLoginMethods: LoginMethod[];
|
enabledLoginMethods: LoginMethod[];
|
||||||
authConsentContent?: string;
|
authConsentContent?: string;
|
||||||
pageFrameContent?: string;
|
pageFrameContent?: string;
|
||||||
|
invalidatingCache: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateServerConfigDTO = {
|
export type TUpdateServerConfigDTO = {
|
||||||
@@ -84,3 +85,16 @@ export enum RootKeyEncryptionStrategy {
|
|||||||
Software = "SOFTWARE",
|
Software = "SOFTWARE",
|
||||||
HSM = "HSM"
|
HSM = "HSM"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CacheType {
|
||||||
|
ALL = "all",
|
||||||
|
SECRETS = "secrets"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TInvalidateCacheDTO = {
|
||||||
|
type: CacheType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetInvalidatingCacheStatus = {
|
||||||
|
invalidating: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
||||||
|
|
||||||
import { AuthPanel } from "./components/AuthPanel";
|
import { AuthPanel } from "./components/AuthPanel";
|
||||||
|
import { CachingPanel } from "./components/CachingPanel";
|
||||||
import { EncryptionPanel } from "./components/EncryptionPanel";
|
import { EncryptionPanel } from "./components/EncryptionPanel";
|
||||||
import { IntegrationPanel } from "./components/IntegrationPanel";
|
import { IntegrationPanel } from "./components/IntegrationPanel";
|
||||||
import { UserPanel } from "./components/UserPanel";
|
import { UserPanel } from "./components/UserPanel";
|
||||||
@@ -42,7 +43,8 @@ enum TabSections {
|
|||||||
Integrations = "integrations",
|
Integrations = "integrations",
|
||||||
Users = "users",
|
Users = "users",
|
||||||
Identities = "identities",
|
Identities = "identities",
|
||||||
Kmip = "kmip"
|
Kmip = "kmip",
|
||||||
|
Caching = "caching"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SignUpModes {
|
enum SignUpModes {
|
||||||
@@ -164,6 +166,7 @@ export const OverviewPage = () => {
|
|||||||
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
||||||
<Tab value={TabSections.Users}>User Identities</Tab>
|
<Tab value={TabSections.Users}>User Identities</Tab>
|
||||||
<Tab value={TabSections.Identities}>Machine Identities</Tab>
|
<Tab value={TabSections.Identities}>Machine Identities</Tab>
|
||||||
|
<Tab value={TabSections.Caching}>Caching</Tab>
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={TabSections.Settings}>
|
<TabPanel value={TabSections.Settings}>
|
||||||
@@ -408,6 +411,9 @@ export const OverviewPage = () => {
|
|||||||
<TabPanel value={TabSections.Identities}>
|
<TabPanel value={TabSections.Identities}>
|
||||||
<IdentityPanel />
|
<IdentityPanel />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Caching}>
|
||||||
|
<CachingPanel />
|
||||||
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Badge, Button, DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { useUser } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useInvalidateCache } from "@app/hooks/api";
|
||||||
|
import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries";
|
||||||
|
import { CacheType } from "@app/hooks/api/admin/types";
|
||||||
|
|
||||||
|
export const CachingPanel = () => {
|
||||||
|
const { mutateAsync: invalidateCache } = useInvalidateCache();
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
const [type, setType] = useState<CacheType | null>(null);
|
||||||
|
const [shouldPoll, setShouldPoll] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: invalidationStatus,
|
||||||
|
isFetching,
|
||||||
|
refetch
|
||||||
|
} = useGetInvalidatingCacheStatus(shouldPoll);
|
||||||
|
const isInvalidating = Boolean(shouldPoll && (isFetching || invalidationStatus));
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"invalidateCache"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const handleInvalidateCacheSubmit = async () => {
|
||||||
|
if (!type || isInvalidating) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invalidateCache({ type });
|
||||||
|
createNotification({ text: `Began invalidating ${type} cache`, type: "success" });
|
||||||
|
setShouldPoll(true);
|
||||||
|
handlePopUpClose("invalidateCache");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInvalidating) return;
|
||||||
|
|
||||||
|
if (shouldPoll) {
|
||||||
|
setShouldPoll(false);
|
||||||
|
createNotification({ text: "Successfully invalidated cache", type: "success" });
|
||||||
|
}
|
||||||
|
}, [isInvalidating, shouldPoll]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetch().then((v) => setShouldPoll(v.data || false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-6 flex flex-wrap items-end justify-between gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="mb-2 flex items-center gap-3">
|
||||||
|
<span className="text-xl font-semibold text-mineshaft-100">Secrets Cache</span>
|
||||||
|
{isInvalidating && (
|
||||||
|
<Badge
|
||||||
|
variant="danger"
|
||||||
|
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
|
||||||
|
Invalidating Cache
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="max-w-xl text-sm text-mineshaft-400">
|
||||||
|
The encrypted secrets cache encompasses all secrets stored within the system and
|
||||||
|
provides a temporary, secure storage location for frequently accessed credentials.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => {
|
||||||
|
setType(CacheType.SECRETS);
|
||||||
|
handlePopUpOpen("invalidateCache");
|
||||||
|
}}
|
||||||
|
isDisabled={!user.superAdmin || isInvalidating}
|
||||||
|
>
|
||||||
|
Invalidate Secrets Cache
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.invalidateCache.isOpen}
|
||||||
|
title={`Are you sure you want to invalidate ${type} cache?`}
|
||||||
|
subTitle="This action is permanent and irreversible. The cache invalidation process may take several minutes to complete."
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("invalidateCache", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={handleInvalidateCacheSubmit}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user