mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 00:28:12 +00:00
Swap to redis lock
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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 ({
|
||||||
|
|||||||
Reference in New Issue
Block a user