Swap to redis lock

This commit is contained in:
x032205
2025-09-01 18:24:55 -04:00
parent a3c6b1134b
commit 21e2db2963
3 changed files with 174 additions and 111 deletions
+1 -1
View File
@@ -37,7 +37,7 @@
"build": "tsup --sourcemap", "build": "tsup --sourcemap",
"build:frontend": "npm run build --prefix ../frontend", "build:frontend": "npm run build --prefix ../frontend",
"start": "node --enable-source-maps dist/main.mjs", "start": "node --enable-source-maps dist/main.mjs",
"type:check": "tsc --noEmit", "type:check": "node --max-old-space-size=8192 ./node_modules/.bin/tsc --noEmit",
"lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src", "lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src",
"lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'", "lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'",
"test:unit": "vitest run -c vitest.unit.config.ts", "test:unit": "vitest run -c vitest.unit.config.ts",
+1
View File
@@ -41,6 +41,7 @@ export const KeyStorePrefixes = {
SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const, SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const,
SecretScanningLock: (dataSourceId: string, resourceExternalId: string) => SecretScanningLock: (dataSourceId: string, resourceExternalId: string) =>
`secret-scanning-v2-mutex-${dataSourceId}-${resourceExternalId}` as const, `secret-scanning-v2-mutex-${dataSourceId}-${resourceExternalId}` as const,
IdentityLockoutLock: (lockoutKey: string) => `identity-lockout-lock-${lockoutKey}` as const,
CaOrderCertificateForSubscriberLock: (subscriberId: string) => CaOrderCertificateForSubscriberLock: (subscriberId: string) =>
`ca-order-certificate-for-subscriber-lock-${subscriberId}` as const, `ca-order-certificate-for-subscriber-lock-${subscriberId}` as const,
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const, SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
@@ -8,11 +8,18 @@ import {
validatePrivilegeChangeOperation validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns"; } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import {
BadRequestError,
NotFoundError,
PermissionBoundaryError,
RateLimitError,
UnauthorizedError
} from "@app/lib/errors";
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
@@ -40,15 +47,18 @@ type TIdentityUaServiceFactoryDep = {
identityOrgMembershipDAL: TIdentityOrgDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems">; keyStore: Pick<
TKeyStoreFactory,
"setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock"
>;
}; };
export type TIdentityUaServiceFactory = ReturnType<typeof identityUaServiceFactory>; export type TIdentityUaServiceFactory = ReturnType<typeof identityUaServiceFactory>;
// type LockoutObject = { type LockoutObject = {
// lockedOut: boolean; lockedOut: boolean;
// failedAttempts: number; failedAttempts: number;
// }; };
export const identityUaServiceFactory = ({ export const identityUaServiceFactory = ({
identityUaDAL, identityUaDAL,
@@ -62,15 +72,8 @@ export const identityUaServiceFactory = ({
const login = async (clientId: string, clientSecret: string, ip: string) => { const login = async (clientId: string, clientSecret: string, ip: string) => {
const identityUa = await identityUaDAL.findOne({ clientId }); const identityUa = await identityUaDAL.findOne({ clientId });
if (!identityUa) { if (!identityUa) {
throw new NotFoundError({ throw new UnauthorizedError({
message: "No identity with specified client ID was found" message: "Invalid credentials"
});
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
if (!identityMembershipOrg) {
throw new NotFoundError({
message: "No identity with the org membership was found"
}); });
} }
@@ -78,15 +81,47 @@ export const identityUaServiceFactory = ({
ipAddress: ip, ipAddress: ip,
trustedIps: identityUa.clientSecretTrustedIps as TIp[] trustedIps: identityUa.clientSecretTrustedIps as TIp[]
}); });
const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`;
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>>;
try {
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 1000);
} catch (e) {
logger.info(`login failed to acquire lock [lockoutKey=${LOCKOUT_KEY}]`);
throw new RateLimitError({ message: "Rate limit exceeded" });
}
try {
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
let lockout: LockoutObject | undefined;
if (lockoutRaw) {
lockout = JSON.parse(lockoutRaw) as LockoutObject;
}
if (lockout && lockout.lockedOut) {
throw new UnauthorizedError({
message: "This identity auth method is temporarily locked, please try again later"
});
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
if (!identityMembershipOrg) {
throw new UnauthorizedError({
message: "Invalid credentials"
});
}
const clientSecretPrefix = clientSecret.slice(0, 4); const clientSecretPrefix = clientSecret.slice(0, 4);
const clientSecrtInfo = await identityUaClientSecretDAL.find({ const clientSecretInfo = await identityUaClientSecretDAL.find({
identityUAId: identityUa.id, identityUAId: identityUa.id,
isClientSecretRevoked: false, isClientSecretRevoked: false,
clientSecretPrefix clientSecretPrefix
}); });
let validClientSecretInfo: (typeof clientSecrtInfo)[0] | null = null; let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
for await (const info of clientSecrtInfo) { for await (const info of clientSecretInfo) {
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash); const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
if (isMatch) { if (isMatch) {
@@ -95,7 +130,31 @@ export const identityUaServiceFactory = ({
} }
} }
if (!validClientSecretInfo) throw new UnauthorizedError({ message: "Invalid credentials" }); if (!validClientSecretInfo) {
if (identityUa.lockoutEnabled) {
if (!lockout) {
lockout = {
lockedOut: false,
failedAttempts: 0
};
}
lockout.failedAttempts += 1;
if (lockout.failedAttempts >= identityUa.lockoutThreshold) {
lockout.lockedOut = true;
}
await keyStore.setItemWithExpiry(
LOCKOUT_KEY,
lockout.lockedOut ? identityUa.lockoutDurationSeconds : identityUa.lockoutCounterResetSeconds,
JSON.stringify(lockout)
);
}
throw new UnauthorizedError({ message: "Invalid credentials" });
} else if (lockout) {
await keyStore.deleteItem(LOCKOUT_KEY);
}
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo; const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
if (Number(clientSecretTTL) > 0) { if (Number(clientSecretTTL) > 0) {
@@ -191,6 +250,9 @@ export const identityUaServiceFactory = ({
identityMembershipOrg, identityMembershipOrg,
...accessTokenTTLParams ...accessTokenTTLParams
}; };
} finally {
await lock.release();
}
}; };
const attachUniversalAuth = async ({ const attachUniversalAuth = async ({