Merge branch 'heads/main' into daniel/rotation-tests

This commit is contained in:
Daniel Hougaard
2025-08-12 02:31:40 +04:00
256 changed files with 9247 additions and 6577 deletions
@@ -0,0 +1,65 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const lastUserLoggedInAuthMethod = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginAuthMethod");
const lastIdentityLoggedInAuthMethod = await knex.schema.hasColumn(
TableName.IdentityOrgMembership,
"lastLoginAuthMethod"
);
const lastUserLoggedInTime = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginTime");
const lastIdentityLoggedInTime = await knex.schema.hasColumn(TableName.IdentityOrgMembership, "lastLoginTime");
if (!lastUserLoggedInAuthMethod || !lastUserLoggedInTime) {
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
if (!lastUserLoggedInAuthMethod) {
t.string("lastLoginAuthMethod").nullable();
}
if (!lastUserLoggedInTime) {
t.datetime("lastLoginTime").nullable();
}
});
}
if (!lastIdentityLoggedInAuthMethod || !lastIdentityLoggedInTime) {
await knex.schema.alterTable(TableName.IdentityOrgMembership, (t) => {
if (!lastIdentityLoggedInAuthMethod) {
t.string("lastLoginAuthMethod").nullable();
}
if (!lastIdentityLoggedInTime) {
t.datetime("lastLoginTime").nullable();
}
});
}
}
export async function down(knex: Knex): Promise<void> {
const lastUserLoggedInAuthMethod = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginAuthMethod");
const lastIdentityLoggedInAuthMethod = await knex.schema.hasColumn(
TableName.IdentityOrgMembership,
"lastLoginAuthMethod"
);
const lastUserLoggedInTime = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginTime");
const lastIdentityLoggedInTime = await knex.schema.hasColumn(TableName.IdentityOrgMembership, "lastLoginTime");
if (lastUserLoggedInAuthMethod || lastUserLoggedInTime) {
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
if (lastUserLoggedInAuthMethod) {
t.dropColumn("lastLoginAuthMethod");
}
if (lastUserLoggedInTime) {
t.dropColumn("lastLoginTime");
}
});
}
if (lastIdentityLoggedInAuthMethod || lastIdentityLoggedInTime) {
await knex.schema.alterTable(TableName.IdentityOrgMembership, (t) => {
if (lastIdentityLoggedInAuthMethod) {
t.dropColumn("lastLoginAuthMethod");
}
if (lastIdentityLoggedInTime) {
t.dropColumn("lastLoginTime");
}
});
}
}
@@ -14,7 +14,9 @@ export const IdentityOrgMembershipsSchema = z.object({
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid()
identityId: z.string().uuid(),
lastLoginAuthMethod: z.string().nullable().optional(),
lastLoginTime: z.date().nullable().optional()
});
export type TIdentityOrgMemberships = z.infer<typeof IdentityOrgMembershipsSchema>;
+3 -1
View File
@@ -19,7 +19,9 @@ export const OrgMembershipsSchema = z.object({
roleId: z.string().uuid().nullable().optional(),
projectFavorites: z.string().array().nullable().optional(),
isActive: z.boolean().default(true),
lastInvitedAt: z.date().nullable().optional()
lastInvitedAt: z.date().nullable().optional(),
lastLoginAuthMethod: z.string().nullable().optional(),
lastLoginTime: z.date().nullable().optional()
});
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
+8 -4
View File
@@ -379,14 +379,17 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/config/:configId/test-connection",
url: "/config/test-connection",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
configId: z.string().trim()
body: z.object({
url: z.string().trim(),
bindDN: z.string().trim(),
bindPass: z.string().trim(),
caCert: z.string().trim()
}),
response: {
200: z.boolean()
@@ -399,8 +402,9 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
ldapConfigId: req.params.configId
...req.body
});
return result;
}
});
@@ -15,6 +15,7 @@ import { z } from "zod";
import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { crypto } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
@@ -170,14 +171,29 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).verifyCredentials(providerInputs.clusterName);
return true;
try {
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).verifyCredentials(providerInputs.clusterName);
return true;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [
providerInputs.accessKeyId,
providerInputs.secretAccessKey,
providerInputs.clusterName,
providerInputs.region
]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: {
@@ -206,21 +222,37 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
const parsedStatement = CreateElastiCacheUserSchema.parse(JSON.parse(creationStatement));
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).createUser(parsedStatement, providerInputs.clusterName);
try {
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).createUser(parsedStatement, providerInputs.clusterName);
return {
entityId: leaseUsername,
data: {
DB_USERNAME: leaseUsername,
DB_PASSWORD: leasePassword
}
};
return {
entityId: leaseUsername,
data: {
DB_USERNAME: leaseUsername,
DB_PASSWORD: leasePassword
}
};
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [
leaseUsername,
leasePassword,
providerInputs.accessKeyId,
providerInputs.secretAccessKey,
providerInputs.clusterName
]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
const revoke = async (inputs: unknown, entityId: string) => {
@@ -229,15 +261,25 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username: entityId });
const parsedStatement = DeleteElasticCacheUserSchema.parse(JSON.parse(revokeStatement));
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).deleteUser(parsedStatement);
try {
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).deleteUser(parsedStatement);
return { entityId };
return { entityId };
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [entityId, providerInputs.accessKeyId, providerInputs.secretAccessKey, providerInputs.clusterName]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
};
const renew = async (_inputs: unknown, entityId: string) => {
@@ -23,6 +23,7 @@ import { CustomAWSHasher } from "@app/lib/aws/hashing";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
@@ -118,22 +119,39 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs, projectId);
const isConnected = await client
.send(new GetUserCommand({}))
.then(() => true)
.catch((err) => {
const message = (err as Error)?.message;
if (
(providerInputs.method === AwsIamAuthType.AssumeRole || providerInputs.method === AwsIamAuthType.IRSA) &&
// assume role will throw an error asking to provider username, but if so this has access in aws correctly
message.includes("Must specify userName when calling with non-User credentials")
) {
return true;
}
throw err;
try {
const client = await $getClient(providerInputs, projectId);
const isConnected = await client
.send(new GetUserCommand({}))
.then(() => true)
.catch((err) => {
const message = (err as Error)?.message;
if (
(providerInputs.method === AwsIamAuthType.AssumeRole || providerInputs.method === AwsIamAuthType.IRSA) &&
// assume role will throw an error asking to provider username, but if so this has access in aws correctly
message.includes("Must specify userName when calling with non-User credentials")
) {
return true;
}
throw err;
});
return isConnected;
} catch (err) {
const sensitiveTokens = [];
if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
}
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
sensitiveTokens.push(providerInputs.roleArn);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: sensitiveTokens
});
return isConnected;
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: {
@@ -162,62 +180,81 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
awsTags.push(...additionalTags);
}
const createUserRes = await client.send(
new CreateUserCommand({
Path: awsPath,
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
Tags: awsTags,
UserName: username
})
);
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
if (userGroups) {
await Promise.all(
userGroups
.split(",")
.filter(Boolean)
.map((group) =>
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
)
);
}
if (policyArns) {
await Promise.all(
policyArns
.split(",")
.filter(Boolean)
.map((policyArn) =>
client.send(new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn }))
)
);
}
if (policyDocument) {
await client.send(
new PutUserPolicyCommand({
UserName: createUserRes.User.UserName,
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
PolicyDocument: policyDocument
try {
const createUserRes = await client.send(
new CreateUserCommand({
Path: awsPath,
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
Tags: awsTags,
UserName: username
})
);
}
const createAccessKeyRes = await client.send(
new CreateAccessKeyCommand({
UserName: createUserRes.User.UserName
})
);
if (!createAccessKeyRes.AccessKey)
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
return {
entityId: username,
data: {
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
USERNAME: username
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
if (userGroups) {
await Promise.all(
userGroups
.split(",")
.filter(Boolean)
.map((group) =>
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
)
);
}
};
if (policyArns) {
await Promise.all(
policyArns
.split(",")
.filter(Boolean)
.map((policyArn) =>
client.send(
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })
)
)
);
}
if (policyDocument) {
await client.send(
new PutUserPolicyCommand({
UserName: createUserRes.User.UserName,
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
PolicyDocument: policyDocument
})
);
}
const createAccessKeyRes = await client.send(
new CreateAccessKeyCommand({
UserName: createUserRes.User.UserName
})
);
if (!createAccessKeyRes.AccessKey)
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
return {
entityId: username,
data: {
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
USERNAME: username
}
};
} catch (err) {
const sensitiveTokens = [username];
if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
}
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
sensitiveTokens.push(providerInputs.roleArn);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: sensitiveTokens
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
@@ -278,8 +315,25 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
)
);
await client.send(new DeleteUserCommand({ UserName: username }));
return { entityId: username };
try {
await client.send(new DeleteUserCommand({ UserName: username }));
return { entityId: username };
} catch (err) {
const sensitiveTokens = [username];
if (providerInputs.method === AwsIamAuthType.AccessKey) {
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
}
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
sensitiveTokens.push(providerInputs.roleArn);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: sensitiveTokens
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
};
const renew = async (_inputs: unknown, entityId: string) => {
@@ -2,6 +2,7 @@ import axios from "axios";
import { customAlphabet } from "nanoid";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { AzureEntraIDSchema, TDynamicProviderFns } from "./models";
@@ -51,45 +52,82 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
return data.success;
try {
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
return data.success;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.clientSecret, providerInputs.applicationId, providerInputs.tenantId]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async ({ inputs }: { inputs: unknown }) => {
const providerInputs = await validateProviderInputs(inputs);
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
if (!data.success) {
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
}
const password = generatePassword();
const response = await axios.patch(
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
{
passwordProfile: {
forceChangePasswordNextSignIn: false,
password
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${data.token}`
}
try {
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
if (!data.success) {
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
}
);
if (response.status !== 204) {
throw new BadRequestError({ message: "Failed to update password" });
}
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
const response = await axios.patch(
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
{
passwordProfile: {
forceChangePasswordNextSignIn: false,
password
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${data.token}`
}
}
);
if (response.status !== 204) {
throw new BadRequestError({ message: "Failed to update password" });
}
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [
providerInputs.clientSecret,
providerInputs.applicationId,
providerInputs.userId,
providerInputs.email,
password
]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
const revoke = async (inputs: unknown, entityId: string) => {
// Creates a new password
await create({ inputs });
return { entityId };
const providerInputs = await validateProviderInputs(inputs);
try {
// Creates a new password
await create({ inputs });
return { entityId };
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.clientSecret, providerInputs.applicationId, entityId]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
};
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
@@ -3,6 +3,8 @@ import handlebars from "handlebars";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@@ -71,9 +73,24 @@ export const CassandraProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
await client.shutdown();
return isConnected;
try {
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
await client.shutdown();
return isConnected;
} catch (err) {
const tokens = [providerInputs.password, providerInputs.username];
if (providerInputs.keyspace) {
tokens.push(providerInputs.keyspace);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens
});
await client.shutdown();
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: {
@@ -89,23 +106,39 @@ export const CassandraProvider = (): TDynamicProviderFns => {
const username = generateUsername(usernameTemplate, identity);
const password = generatePassword();
const { keyspace } = providerInputs;
const expiration = new Date(expireAt).toISOString();
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration,
keyspace
});
try {
const expiration = new Date(expireAt).toISOString();
const queries = creationStatement.toString().split(";").filter(Boolean);
for (const query of queries) {
// eslint-disable-next-line
await client.execute(query);
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration,
keyspace
});
const queries = creationStatement.toString().split(";").filter(Boolean);
for (const query of queries) {
// eslint-disable-next-line
await client.execute(query);
}
await client.shutdown();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
} catch (err) {
const tokens = [username, password];
if (keyspace) {
tokens.push(keyspace);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens
});
await client.shutdown();
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
await client.shutdown();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
@@ -115,14 +148,29 @@ export const CassandraProvider = (): TDynamicProviderFns => {
const username = entityId;
const { keyspace } = providerInputs;
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace });
const queries = revokeStatement.toString().split(";").filter(Boolean);
for (const query of queries) {
// eslint-disable-next-line
await client.execute(query);
try {
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace });
const queries = revokeStatement.toString().split(";").filter(Boolean);
for (const query of queries) {
// eslint-disable-next-line
await client.execute(query);
}
await client.shutdown();
return { entityId: username };
} catch (err) {
const tokens = [username];
if (keyspace) {
tokens.push(keyspace);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens
});
await client.shutdown();
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
await client.shutdown();
return { entityId: username };
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
@@ -130,21 +178,36 @@ export const CassandraProvider = (): TDynamicProviderFns => {
if (!providerInputs.renewStatement) return { entityId };
const client = await $getClient(providerInputs);
const expiration = new Date(expireAt).toISOString();
const { keyspace } = providerInputs;
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username: entityId,
keyspace,
expiration
});
const queries = renewStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await client.execute(query);
try {
const expiration = new Date(expireAt).toISOString();
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username: entityId,
keyspace,
expiration
});
const queries = renewStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await client.execute(query);
}
await client.shutdown();
return { entityId };
} catch (err) {
const tokens = [entityId];
if (keyspace) {
tokens.push(keyspace);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens
});
await client.shutdown();
throw new BadRequestError({
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
});
}
await client.shutdown();
return { entityId };
};
return {
@@ -2,6 +2,8 @@ import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
@@ -63,12 +65,24 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs);
const infoResponse = await connection
.info()
.then(() => true)
.catch(() => false);
return infoResponse;
try {
const infoResponse = await connection.info().then(() => true);
return infoResponse;
} catch (err) {
const tokens = [];
if (providerInputs.auth.type === ElasticSearchAuthTypes.ApiKey) {
tokens.push(providerInputs.auth.apiKey, providerInputs.auth.apiKeyId);
} else {
tokens.push(providerInputs.auth.username, providerInputs.auth.password);
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
@@ -79,27 +93,49 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
const username = generateUsername(usernameTemplate, identity);
const password = generatePassword();
await connection.security.putUser({
username,
password,
full_name: "Managed by Infisical.com",
roles: providerInputs.roles
});
try {
await connection.security.putUser({
username,
password,
full_name: "Managed by Infisical.com",
roles: providerInputs.roles
});
await connection.close();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
await connection.close();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password]
});
await connection.close();
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs);
await connection.security.deleteUser({
username: entityId
});
try {
await connection.security.deleteUser({
username: entityId
});
await connection.close();
return { entityId };
await connection.close();
return { entityId };
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [entityId]
});
await connection.close();
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
};
const renew = async (_inputs: unknown, entityId: string) => {
@@ -3,6 +3,7 @@ import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretGcpIamSchema, TDynamicProviderFns } from "./models";
@@ -65,8 +66,18 @@ export const GcpIamProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
await $getToken(providerInputs.serviceAccountEmail, 10);
return true;
try {
await $getToken(providerInputs.serviceAccountEmail, 10);
return true;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.serviceAccountEmail]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: { inputs: unknown; expireAt: number }) => {
@@ -74,13 +85,23 @@ export const GcpIamProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs);
const now = Math.floor(Date.now() / 1000);
const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0);
try {
const now = Math.floor(Date.now() / 1000);
const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0);
const token = await $getToken(providerInputs.serviceAccountEmail, ttl);
const entityId = alphaNumericNanoId(32);
const token = await $getToken(providerInputs.serviceAccountEmail, ttl);
const entityId = alphaNumericNanoId(32);
return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } };
return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } };
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.serviceAccountEmail]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
const revoke = async (_inputs: unknown, entityId: string) => {
@@ -89,10 +110,21 @@ export const GcpIamProvider = (): TDynamicProviderFns => {
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
// To renew a token it must be re-created
const data = await create({ inputs, expireAt });
try {
// To renew a token it must be re-created
const data = await create({ inputs, expireAt });
return { ...data, entityId };
return { ...data, entityId };
} catch (err) {
const providerInputs = await validateProviderInputs(inputs);
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.serviceAccountEmail]
});
throw new BadRequestError({
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
});
}
};
return {
@@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
@@ -89,26 +90,46 @@ export const GithubProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
await $generateGitHubInstallationAccessToken(providerInputs);
return true;
try {
await $generateGitHubInstallationAccessToken(providerInputs);
return true;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.privateKey, String(providerInputs.appId), String(providerInputs.installationId)]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: { inputs: unknown }) => {
const { inputs } = data;
const providerInputs = await validateProviderInputs(inputs);
const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs);
const entityId = alphaNumericNanoId(32);
try {
const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs);
const entityId = alphaNumericNanoId(32);
return {
entityId,
data: {
TOKEN: ghTokenData.token,
EXPIRES_AT: ghTokenData.expires_at,
PERMISSIONS: ghTokenData.permissions,
REPOSITORY_SELECTION: ghTokenData.repository_selection
}
};
return {
entityId,
data: {
TOKEN: ghTokenData.token,
EXPIRES_AT: ghTokenData.expires_at,
PERMISSIONS: ghTokenData.permissions,
REPOSITORY_SELECTION: ghTokenData.repository_selection
}
};
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.privateKey, String(providerInputs.appId), String(providerInputs.installationId)]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
const revoke = async () => {
@@ -2,7 +2,8 @@ import axios, { AxiosError } from "axios";
import handlebars from "handlebars";
import https from "https";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
@@ -356,8 +357,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
errorMessage = (error.response?.data as { message: string }).message;
}
throw new InternalServerError({
message: `Failed to validate connection: ${errorMessage}`
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: errorMessage,
tokens: [providerInputs.clusterToken || ""]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
@@ -602,8 +607,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
errorMessage = (error.response?.data as { message: string }).message;
}
throw new InternalServerError({
message: `Failed to create dynamic secret: ${errorMessage}`
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: errorMessage,
tokens: [providerInputs.clusterToken || ""]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
@@ -683,50 +692,65 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
};
if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) {
const rawUrl =
providerInputs.authMethod === KubernetesAuthMethod.Gateway
? GATEWAY_AUTH_DEFAULT_URL
: providerInputs.url || "";
try {
const rawUrl =
providerInputs.authMethod === KubernetesAuthMethod.Gateway
? GATEWAY_AUTH_DEFAULT_URL
: providerInputs.url || "";
const url = new URL(rawUrl);
const k8sGatewayHost = url.hostname;
const k8sPort = url.port ? Number(url.port) : 443;
const k8sHost = `${url.protocol}//${url.hostname}`;
const url = new URL(rawUrl);
const k8sGatewayHost = url.hostname;
const k8sPort = url.port ? Number(url.port) : 443;
const k8sHost = `${url.protocol}//${url.hostname}`;
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
await $gatewayProxyWrapper(
{
gatewayId: providerInputs.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort,
httpsAgent,
reviewTokenThroughGateway: true
},
serviceAccountDynamicCallback
);
if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
await $gatewayProxyWrapper(
{
gatewayId: providerInputs.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort,
httpsAgent,
reviewTokenThroughGateway: true
},
serviceAccountDynamicCallback
);
} else {
await $gatewayProxyWrapper(
{
gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost,
targetPort: k8sPort,
httpsAgent,
reviewTokenThroughGateway: false
},
serviceAccountDynamicCallback
);
}
} else {
await $gatewayProxyWrapper(
{
gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost,
targetPort: k8sPort,
httpsAgent,
reviewTokenThroughGateway: false
},
serviceAccountDynamicCallback
);
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
}
} else {
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
} catch (error) {
let errorMessage = error instanceof Error ? error.message : "Unknown error";
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
errorMessage = (error.response?.data as { message: string }).message;
}
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: errorMessage,
tokens: [entityId, providerInputs.clusterToken || ""]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
}
@@ -6,6 +6,7 @@ import RE2 from "re2";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models";
@@ -91,8 +92,18 @@ export const LdapProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
return client.connected;
try {
const client = await $getClient(providerInputs);
return client.connected;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.bindpass, providerInputs.binddn]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const executeLdif = async (client: ldapjs.Client, ldif_file: string) => {
@@ -205,11 +216,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnRegex = new RE2("^dn:\\s*(.+)", "m");
const dnMatch = dnRegex.exec(providerInputs.rotationLdif);
const username = dnMatch?.[1];
if (!username) throw new BadRequestError({ message: "Username not found from Ldif" });
const password = generatePassword();
if (dnMatch) {
const username = dnMatch[1];
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
try {
@@ -217,7 +228,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
throw new BadRequestError({ message: (err as Error).message });
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, providerInputs.binddn, providerInputs.bindpass]
});
throw new BadRequestError({ message: sanitizedErrorMessage });
}
} else {
throw new BadRequestError({
@@ -238,7 +253,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
await executeLdif(client, rollbackLdif);
}
throw new BadRequestError({ message: (err as Error).message });
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, providerInputs.binddn, providerInputs.bindpass]
});
throw new BadRequestError({ message: sanitizedErrorMessage });
}
}
};
@@ -262,7 +281,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
throw new BadRequestError({ message: (err as Error).message });
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, providerInputs.binddn, providerInputs.bindpass]
});
throw new BadRequestError({ message: sanitizedErrorMessage });
}
} else {
throw new BadRequestError({
@@ -278,7 +301,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
const renew = async (_inputs: unknown, entityId: string) => {
// No renewal necessary
return { entityId };
};
@@ -3,6 +3,8 @@ import { customAlphabet } from "nanoid";
import { z } from "zod";
import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
@@ -49,19 +51,25 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
const isConnected = await client({
method: "GET",
url: `v2/groups/${providerInputs.groupId}/databaseUsers`,
params: { itemsPerPage: 1 }
})
.then(() => true)
.catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
try {
const isConnected = await client({
method: "GET",
url: `v2/groups/${providerInputs.groupId}/databaseUsers`,
params: { itemsPerPage: 1 }
}).then(() => true);
return isConnected;
} catch (error) {
const errorMessage = (error as AxiosError).response
? JSON.stringify((error as AxiosError).response?.data)
: (error as Error)?.message;
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: errorMessage,
tokens: [providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId]
});
return isConnected;
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: {
@@ -77,25 +85,39 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
const username = generateUsername(usernameTemplate, identity);
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
await client({
method: "POST",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers`,
data: {
roles: providerInputs.roles,
scopes: providerInputs.scopes,
deleteAfterDate: expiration,
username,
password,
databaseName: "admin",
groupId: providerInputs.groupId
}
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
try {
await client({
method: "POST",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers`,
data: {
roles: providerInputs.roles,
scopes: providerInputs.scopes,
deleteAfterDate: expiration,
username,
password,
databaseName: "admin",
groupId: providerInputs.groupId
}
});
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
} catch (error) {
const errorMessage = (error as AxiosError).response
? JSON.stringify((error as AxiosError).response?.data)
: (error as Error)?.message;
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: errorMessage,
tokens: [
username,
password,
providerInputs.adminPublicKey,
providerInputs.adminPrivateKey,
providerInputs.groupId
]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
const revoke = async (inputs: unknown, entityId: string) => {
@@ -111,15 +133,23 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
throw err;
});
if (isExisting) {
await client({
method: "DELETE",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
try {
await client({
method: "DELETE",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
});
} catch (error) {
const errorMessage = (error as AxiosError).response
? JSON.stringify((error as AxiosError).response?.data)
: (error as Error)?.message;
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: errorMessage,
tokens: [username, providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
}
return { entityId: username };
@@ -132,21 +162,29 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
const username = entityId;
const expiration = new Date(expireAt).toISOString();
await client({
method: "PATCH",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`,
data: {
deleteAfterDate: expiration,
databaseName: "admin",
groupId: providerInputs.groupId
}
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return { entityId: username };
try {
await client({
method: "PATCH",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`,
data: {
deleteAfterDate: expiration,
databaseName: "admin",
groupId: providerInputs.groupId
}
});
return { entityId: username };
} catch (error) {
const errorMessage = (error as AxiosError).response
? JSON.stringify((error as AxiosError).response?.data)
: (error as Error)?.message;
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: errorMessage,
tokens: [username, providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId]
});
throw new BadRequestError({
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
});
}
};
return {
@@ -2,6 +2,8 @@ import { MongoClient } from "mongodb";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
@@ -51,13 +53,24 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
const isConnected = await client
.db(providerInputs.database)
.command({ ping: 1 })
.then(() => true);
try {
const isConnected = await client
.db(providerInputs.database)
.command({ ping: 1 })
.then(() => true);
await client.close();
return isConnected;
await client.close();
return isConnected;
} catch (err) {
await client.close();
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.password, providerInputs.username, providerInputs.database, providerInputs.host]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
@@ -68,16 +81,27 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
const username = generateUsername(usernameTemplate, identity);
const password = generatePassword();
const db = client.db(providerInputs.database);
try {
const db = client.db(providerInputs.database);
await db.command({
createUser: username,
pwd: password,
roles: providerInputs.roles
});
await client.close();
await db.command({
createUser: username,
pwd: password,
roles: providerInputs.roles
});
await client.close();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
} catch (err) {
await client.close();
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, providerInputs.password, providerInputs.username, providerInputs.database]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
const revoke = async (inputs: unknown, entityId: string) => {
@@ -86,13 +110,24 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
const username = entityId;
const db = client.db(providerInputs.database);
await db.command({
dropUser: username
});
await client.close();
try {
const db = client.db(providerInputs.database);
await db.command({
dropUser: username
});
await client.close();
return { entityId: username };
return { entityId: username };
} catch (err) {
await client.close();
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, providerInputs.password, providerInputs.username, providerInputs.database]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
};
const renew = async (_inputs: unknown, entityId: string) => {
@@ -3,6 +3,8 @@ import https from "https";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -110,11 +112,19 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs);
const infoResponse = await connection.get("/whoami").then(() => true);
return infoResponse;
try {
const connection = await $getClient(providerInputs);
const infoResponse = await connection.get("/whoami").then(() => true);
return infoResponse;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.password, providerInputs.username, providerInputs.host]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
@@ -125,26 +135,44 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
const username = generateUsername(usernameTemplate, identity);
const password = generatePassword();
await createRabbitMqUser({
axiosInstance: connection,
virtualHost: providerInputs.virtualHost,
createUser: {
password,
username,
tags: [...(providerInputs.tags ?? []), "infisical-user"]
}
});
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
try {
await createRabbitMqUser({
axiosInstance: connection,
virtualHost: providerInputs.virtualHost,
createUser: {
password,
username,
tags: [...(providerInputs.tags ?? []), "infisical-user"]
}
});
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, providerInputs.password, providerInputs.username]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs);
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
return { entityId };
try {
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
return { entityId };
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [entityId, providerInputs.password, providerInputs.username]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
};
const renew = async (_inputs: unknown, entityId: string) => {
@@ -4,6 +4,7 @@ import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@@ -112,14 +113,27 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await $getClient(providerInputs);
const pingResponse = await connection
.ping()
.then(() => true)
.catch(() => false);
return pingResponse;
let connection;
try {
connection = await $getClient(providerInputs);
const pingResponse = await connection.ping().then(() => true);
await connection.quit();
return pingResponse;
} catch (err) {
if (connection) await connection.quit();
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [
providerInputs.password || "",
providerInputs.username,
providerInputs.host,
String(providerInputs.port)
]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: {
@@ -144,10 +158,20 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const queries = creationStatement.toString().split(";").filter(Boolean);
await executeTransactions(connection, queries);
await connection.quit();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
try {
await executeTransactions(connection, queries);
await connection.quit();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
} catch (err) {
await connection.quit();
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, providerInputs.password || "", providerInputs.username]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
};
const revoke = async (inputs: unknown, entityId: string) => {
@@ -159,10 +183,20 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
const queries = revokeStatement.toString().split(";").filter(Boolean);
await executeTransactions(connection, queries);
await connection.quit();
return { entityId: username };
try {
await executeTransactions(connection, queries);
await connection.quit();
return { entityId: username };
} catch (err) {
await connection.quit();
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, providerInputs.password || "", providerInputs.username]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
@@ -176,13 +210,23 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await executeTransactions(connection, queries);
try {
if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await executeTransactions(connection, queries);
}
await connection.quit();
return { entityId: username };
} catch (err) {
await connection.quit();
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, providerInputs.password || "", providerInputs.username]
});
throw new BadRequestError({
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
});
}
await connection.quit();
return { entityId: username };
};
return {
@@ -4,6 +4,7 @@ import odbc from "odbc";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@@ -67,25 +68,41 @@ export const SapAseProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const masterClient = await $getClient(providerInputs, true);
const client = await $getClient(providerInputs);
let masterClient;
let client;
try {
masterClient = await $getClient(providerInputs, true);
client = await $getClient(providerInputs);
const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version");
const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version");
const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version");
const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version");
if (!resultFromSelectedDatabase.version) {
if (!resultFromSelectedDatabase.version) {
throw new BadRequestError({
message: "Failed to validate SAP ASE connection, version query failed"
});
}
if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) {
throw new BadRequestError({
message: "Failed to validate SAP ASE connection (master), version mismatch"
});
}
await masterClient.close();
await client.close();
return true;
} catch (err) {
if (masterClient) await masterClient.close();
if (client) await client.close();
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.password, providerInputs.username, providerInputs.host, providerInputs.database]
});
throw new BadRequestError({
message: "Failed to validate SAP ASE connection, version query failed"
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) {
throw new BadRequestError({
message: "Failed to validate SAP ASE connection (master), version mismatch"
});
}
return true;
};
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
@@ -105,16 +122,26 @@ export const SapAseProvider = (): TDynamicProviderFns => {
const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
for await (const query of queries) {
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
// If not done, then the newly created user won't be able to authenticate.
await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query);
try {
for await (const query of queries) {
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
// If not done, then the newly created user won't be able to authenticate.
await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query);
}
await masterClient.close();
await client.close();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
} catch (err) {
await masterClient.close();
await client.close();
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, providerInputs.password, providerInputs.username, providerInputs.database]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
await masterClient.close();
await client.close();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, username: string) => {
@@ -140,14 +167,24 @@ export const SapAseProvider = (): TDynamicProviderFns => {
}
}
for await (const query of queries) {
await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query);
try {
for await (const query of queries) {
await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query);
}
await masterClient.close();
await client.close();
return { entityId: username };
} catch (err) {
await masterClient.close();
await client.close();
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, providerInputs.password, providerInputs.username, providerInputs.database]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
await masterClient.close();
await client.close();
return { entityId: username };
};
const renew = async (_: unknown, username: string) => {
@@ -10,6 +10,7 @@ import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@@ -83,19 +84,26 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
const testResult = await new Promise<boolean>((resolve, reject) => {
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
if (err) {
reject();
}
resolve(true);
try {
const client = await $getClient(providerInputs);
const testResult = await new Promise<boolean>((resolve, reject) => {
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
if (err) {
return reject(err);
}
resolve(true);
});
});
});
return testResult;
return testResult;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.password, providerInputs.username, providerInputs.host]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (data: {
@@ -119,18 +127,22 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
});
const queries = creationStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) {
reject(
new BadRequestError({
message: err.message
})
);
}
resolve(true);
try {
for await (const query of queries) {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) return reject(err);
resolve(true);
});
});
}
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, providerInputs.password, providerInputs.username]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
@@ -142,18 +154,24 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
const client = await $getClient(providerInputs);
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
const queries = revokeStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) {
reject(
new BadRequestError({
message: err.message
})
);
}
resolve(true);
try {
for await (const query of queries) {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) {
reject(err);
}
resolve(true);
});
});
}
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, providerInputs.password, providerInputs.username]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
}
@@ -174,16 +192,20 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
await new Promise((resolve, reject) => {
client.exec(query, (err: any) => {
if (err) {
reject(
new BadRequestError({
message: err.message
})
);
reject(err);
}
resolve(true);
});
});
}
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [entityId, providerInputs.password, providerInputs.username]
});
throw new BadRequestError({
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
});
} finally {
client.disconnect();
}
@@ -4,6 +4,7 @@ import snowflake from "snowflake-sdk";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@@ -69,12 +70,10 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await $getClient(providerInputs);
let isValidConnection: boolean;
let client;
try {
isValidConnection = await Promise.race([
client = await $getClient(providerInputs);
const isValidConnection = await Promise.race([
client.isValidAsync(),
new Promise((resolve) => {
setTimeout(resolve, 10000);
@@ -82,11 +81,18 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
throw new BadRequestError({ message: "Unable to establish connection - verify credentials" });
})
]);
return isValidConnection;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.password, providerInputs.username, providerInputs.accountId, providerInputs.orgId]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
} finally {
client.destroy(noop);
if (client) client.destroy(noop);
}
return isValidConnection;
};
const create = async (data: {
@@ -116,13 +122,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
sqlText: creationStatement,
complete(err) {
if (err) {
return reject(new BadRequestError({ name: "CreateLease", message: err.message }));
return reject(err);
}
return resolve(true);
}
});
});
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error).message,
tokens: [username, password, providerInputs.password, providerInputs.username]
});
throw new BadRequestError({ message: `Failed to create lease from provider: ${sanitizedErrorMessage}` });
} finally {
client.destroy(noop);
}
@@ -143,13 +155,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
sqlText: revokeStatement,
complete(err) {
if (err) {
return reject(new BadRequestError({ name: "RevokeLease", message: err.message }));
return reject(err);
}
return resolve(true);
}
});
});
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error).message,
tokens: [username, providerInputs.password, providerInputs.username]
});
throw new BadRequestError({ message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` });
} finally {
client.destroy(noop);
}
@@ -175,13 +193,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
sqlText: renewStatement,
complete(err) {
if (err) {
return reject(new BadRequestError({ name: "RenewLease", message: err.message }));
return reject(err);
}
return resolve(true);
}
});
});
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error).message,
tokens: [entityId, providerInputs.password, providerInputs.username]
});
throw new BadRequestError({ message: `Failed to renew lease from provider: ${sanitizedErrorMessage}` });
} finally {
client.destroy(noop);
}
@@ -3,6 +3,8 @@ import knex from "knex";
import { z } from "zod";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
@@ -212,8 +214,19 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
// oracle needs from keyword
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
isConnected = await db.raw(testStatement).then(() => true);
await db.destroy();
try {
isConnected = await db.raw(testStatement).then(() => true);
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.username]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
} finally {
await db.destroy();
}
};
if (providerInputs.gatewayId) {
@@ -233,13 +246,13 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
const { inputs, expireAt, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs);
const { database } = providerInputs;
const username = generateUsername(providerInputs.client, usernameTemplate, identity);
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
try {
const { database } = providerInputs;
const expiration = new Date(expireAt).toISOString();
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
@@ -256,6 +269,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await tx.raw(query);
}
});
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, database]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
} finally {
await db.destroy();
}
@@ -283,6 +304,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await tx.raw(query);
}
});
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, database]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
} finally {
await db.destroy();
}
@@ -319,6 +348,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
}
});
}
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [database]
});
throw new BadRequestError({
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
});
} finally {
await db.destroy();
}
@@ -1,6 +1,8 @@
import { authenticator } from "otplib";
import { HashAlgorithms } from "otplib/core";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
@@ -12,62 +14,84 @@ export const TotpProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const validateConnection = async () => {
return true;
const validateConnection = async (inputs: unknown) => {
try {
await validateProviderInputs(inputs);
return true;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: []
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
}
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const create = async (data: { inputs: unknown }) => {
const { inputs } = data;
try {
const providerInputs = await validateProviderInputs(inputs);
const entityId = alphaNumericNanoId(32);
const authenticatorInstance = authenticator.clone();
const entityId = alphaNumericNanoId(32);
const authenticatorInstance = authenticator.clone();
let secret: string;
let period: number | null | undefined;
let digits: number | null | undefined;
let algorithm: HashAlgorithms | null | undefined;
let secret: string;
let period: number | null | undefined;
let digits: number | null | undefined;
let algorithm: HashAlgorithms | null | undefined;
if (providerInputs.configType === TotpConfigType.URL) {
const urlObj = new URL(providerInputs.url);
secret = urlObj.searchParams.get("secret") as string;
const periodFromUrl = urlObj.searchParams.get("period");
const digitsFromUrl = urlObj.searchParams.get("digits");
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
if (providerInputs.configType === TotpConfigType.URL) {
const urlObj = new URL(providerInputs.url);
secret = urlObj.searchParams.get("secret") as string;
const periodFromUrl = urlObj.searchParams.get("period");
const digitsFromUrl = urlObj.searchParams.get("digits");
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
if (periodFromUrl) {
period = +periodFromUrl;
if (periodFromUrl) {
period = +periodFromUrl;
}
if (digitsFromUrl) {
digits = +digitsFromUrl;
}
if (algorithmFromUrl) {
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
}
} else {
secret = providerInputs.secret;
period = providerInputs.period;
digits = providerInputs.digits;
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
}
if (digitsFromUrl) {
digits = +digitsFromUrl;
if (digits) {
authenticatorInstance.options = { digits };
}
if (algorithmFromUrl) {
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
if (algorithm) {
authenticatorInstance.options = { algorithm };
}
} else {
secret = providerInputs.secret;
period = providerInputs.period;
digits = providerInputs.digits;
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
}
if (digits) {
authenticatorInstance.options = { digits };
}
if (period) {
authenticatorInstance.options = { step: period };
}
if (algorithm) {
authenticatorInstance.options = { algorithm };
return {
entityId,
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
};
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: []
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
}
if (period) {
authenticatorInstance.options = { step: period };
}
return {
entityId,
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
};
};
const revoke = async (_inputs: unknown, entityId: string) => {
@@ -4,6 +4,7 @@ import { z } from "zod";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -275,6 +276,14 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam
await client.raw(trimmedQuery);
}
}
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, providerInputs.username, providerInputs.password]
});
throw new BadRequestError({
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
});
} finally {
if (client) await client.destroy();
}
@@ -339,6 +348,14 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam
await client.raw(trimmedQuery);
}
}
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, providerInputs.username, providerInputs.password]
});
throw new BadRequestError({
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
});
} finally {
if (client) await client.destroy();
}
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { Knex } from "knex";
import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
@@ -45,7 +46,7 @@ import { searchGroups, testLDAPConfig } from "./ldap-fns";
import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
type TLdapConfigServiceFactoryDep = {
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne">;
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne" | "transaction">;
ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgDAL: Pick<
@@ -131,6 +132,19 @@ export const ldapConfigServiceFactory = ({
orgId
});
const isConnected = await testLDAPConfig({
bindDN,
bindPass,
caCert,
url
});
if (!isConnected) {
throw new BadRequestError({
message: "Failed to establish connection to LDAP directory. Please verify that your credentials are correct."
});
}
const ldapConfig = await ldapConfigDAL.create({
orgId,
isActive,
@@ -148,6 +162,50 @@ export const ldapConfigServiceFactory = ({
return ldapConfig;
};
const getLdapCfg = async (filter: { orgId: string; isActive?: boolean; id?: string }, tx?: Knex) => {
const ldapConfig = await ldapConfigDAL.findOne(filter, tx);
if (!ldapConfig) {
throw new NotFoundError({
message: `Failed to find organization LDAP data in organization with ID '${filter.orgId}'`
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: ldapConfig.orgId
});
let bindDN = "";
if (ldapConfig.encryptedLdapBindDN) {
bindDN = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindDN }).toString();
}
let bindPass = "";
if (ldapConfig.encryptedLdapBindPass) {
bindPass = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindPass }).toString();
}
let caCert = "";
if (ldapConfig.encryptedLdapCaCertificate) {
caCert = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapCaCertificate }).toString();
}
return {
id: ldapConfig.id,
organization: ldapConfig.orgId,
isActive: ldapConfig.isActive,
url: ldapConfig.url,
bindDN,
bindPass,
uniqueUserAttribute: ldapConfig.uniqueUserAttribute,
searchBase: ldapConfig.searchBase,
searchFilter: ldapConfig.searchFilter,
groupSearchBase: ldapConfig.groupSearchBase,
groupSearchFilter: ldapConfig.groupSearchFilter,
caCert
};
};
const updateLdapCfg = async ({
actor,
actorId,
@@ -202,53 +260,25 @@ export const ldapConfigServiceFactory = ({
updateQuery.encryptedLdapCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob;
}
const [ldapConfig] = await ldapConfigDAL.update({ orgId }, updateQuery);
const config = await ldapConfigDAL.transaction(async (tx) => {
const [updatedLdapCfg] = await ldapConfigDAL.update({ orgId }, updateQuery, tx);
const decryptedLdapCfg = await getLdapCfg({ orgId }, tx);
return ldapConfig;
};
const isSoftDeletion = !decryptedLdapCfg.url && !decryptedLdapCfg.bindDN && !decryptedLdapCfg.bindPass;
if (!isSoftDeletion) {
const isConnected = await testLDAPConfig(decryptedLdapCfg);
if (!isConnected) {
throw new BadRequestError({
message:
"Failed to establish connection to LDAP directory. Please verify that your credentials are correct."
});
}
}
const getLdapCfg = async (filter: { orgId: string; isActive?: boolean; id?: string }) => {
const ldapConfig = await ldapConfigDAL.findOne(filter);
if (!ldapConfig) {
throw new NotFoundError({
message: `Failed to find organization LDAP data in organization with ID '${filter.orgId}'`
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: ldapConfig.orgId
return updatedLdapCfg;
});
let bindDN = "";
if (ldapConfig.encryptedLdapBindDN) {
bindDN = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindDN }).toString();
}
let bindPass = "";
if (ldapConfig.encryptedLdapBindPass) {
bindPass = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapBindPass }).toString();
}
let caCert = "";
if (ldapConfig.encryptedLdapCaCertificate) {
caCert = decryptor({ cipherTextBlob: ldapConfig.encryptedLdapCaCertificate }).toString();
}
return {
id: ldapConfig.id,
organization: ldapConfig.orgId,
isActive: ldapConfig.isActive,
url: ldapConfig.url,
bindDN,
bindPass,
uniqueUserAttribute: ldapConfig.uniqueUserAttribute,
searchBase: ldapConfig.searchBase,
searchFilter: ldapConfig.searchFilter,
groupSearchBase: ldapConfig.groupSearchBase,
groupSearchFilter: ldapConfig.groupSearchFilter,
caCert
};
return config;
};
const getLdapCfgWithPermissionCheck = async ({
@@ -527,14 +557,13 @@ export const ldapConfigServiceFactory = ({
});
const isUserCompleted = Boolean(user.isAccepted);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = crypto.jwt().sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
username: user.username,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
hasExchangedPrivateKey: true,
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
firstName,
lastName,
@@ -694,7 +723,17 @@ export const ldapConfigServiceFactory = ({
return deletedGroupMap;
};
const testLDAPConnection = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TTestLdapConnectionDTO) => {
const testLDAPConnection = async ({
actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId,
bindDN,
bindPass,
caCert,
url
}: TTestLdapConnectionDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
@@ -704,11 +743,12 @@ export const ldapConfigServiceFactory = ({
message: "Failed to test LDAP connection due to plan restriction. Upgrade plan to test the LDAP connection."
});
const ldapConfig = await getLdapCfg({
orgId
return testLDAPConfig({
bindDN,
bindPass,
caCert,
url
});
return testLDAPConfig(ldapConfig);
};
return {
@@ -83,6 +83,4 @@ export type TDeleteLdapGroupMapDTO = {
ldapGroupMapId: string;
} & TOrgPermission;
export type TTestLdapConnectionDTO = {
ldapConfigId: string;
} & TOrgPermission;
export type TTestLdapConnectionDTO = TOrgPermission & TTestLDAPConfigDTO;
@@ -404,7 +404,6 @@ export const oidcConfigServiceFactory = ({
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const isUserCompleted = Boolean(user.isAccepted);
const providerAuthToken = crypto.jwt().sign(
{
@@ -417,7 +416,7 @@ export const oidcConfigServiceFactory = ({
organizationName: organization.name,
organizationId: organization.id,
organizationSlug: organization.slug,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
hasExchangedPrivateKey: true,
authMethod: AuthMethod.OIDC,
authType: UserAliasType.OIDC,
isUserCompleted,
@@ -411,7 +411,6 @@ export const samlConfigServiceFactory = ({
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = crypto.jwt().sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
@@ -424,7 +423,7 @@ export const samlConfigServiceFactory = ({
organizationId: organization.id,
organizationSlug: organization.slug,
authMethod: authProvider,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
hasExchangedPrivateKey: true,
authType: UserAliasType.SAML,
isUserCompleted,
...(relayState
@@ -58,9 +58,9 @@ export function scanDirectory(inputPath: string, outputPath: string, configPath?
});
}
export function scanFile(inputPath: string): Promise<void> {
export function scanFile(inputPath: string, configPath?: string): Promise<void> {
return new Promise((resolve, reject) => {
const command = `infisical scan --exit-code=77 --source "${inputPath}" --no-git`;
const command = `infisical scan --exit-code=77 --source "${inputPath}" --no-git ${configPath ? `-c ${configPath}` : ""}`;
exec(command, (error) => {
if (error && error.code === 77) {
reject(error);
@@ -166,6 +166,20 @@ export const parseScanErrorMessage = (err: unknown): string => {
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
};
const generateSecretValuePolicyConfiguration = (entropy: number): string => `
# Extend default configuration to preserve existing rules
[extend]
useDefault = true
# Add custom high-entropy rule
[[rules]]
id = "high-entropy"
description = "Will scan for high entropy secrets"
regex = '''.*'''
entropy = ${entropy}
keywords = []
`;
export const scanSecretPolicyViolations = async (
projectId: string,
secretPath: string,
@@ -188,14 +202,25 @@ export const scanSecretPolicyViolations = async (
const tempFolder = await createTempFolder();
try {
const configPath = join(tempFolder, "infisical-scan.toml");
const secretPolicyConfiguration = generateSecretValuePolicyConfiguration(
appCfg.PARAMS_FOLDER_SECRET_DETECTION_ENTROPY
);
await writeTextToFile(configPath, secretPolicyConfiguration);
const scanPromises = secrets
.filter((secret) => !ignoreValues.includes(secret.secretValue))
.map(async (secret) => {
const secretFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`);
await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`);
const secretKeyValueFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`);
const secretValueOnlyFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`);
await writeTextToFile(secretKeyValueFilePath, `${secret.secretKey}=${secret.secretValue}`);
await writeTextToFile(secretValueOnlyFilePath, secret.secretValue);
try {
await scanFile(secretFilePath);
await scanFile(secretKeyValueFilePath);
await scanFile(secretValueOnlyFilePath, configPath);
} catch (error) {
throw new BadRequestError({
message: `Secret value detected in ${secret.secretKey}. Please add this instead to the designated secrets path in the project.`,
+4
View File
@@ -79,6 +79,7 @@ const envSchema = z
QUEUE_WORKER_PROFILE: z.nativeEnum(QueueWorkerProfile).default(QueueWorkerProfile.All),
HTTPS_ENABLED: zodStrBool,
ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
// smtp options
SMTP_HOST: zpStr(z.string().optional()),
SMTP_IGNORE_TLS: zodStrBool.default("false"),
@@ -215,6 +216,7 @@ const envSchema = z
return JSON.parse(val) as { secretPath: string; projectId: string }[];
})
),
PARAMS_FOLDER_SECRET_DETECTION_ENTROPY: z.coerce.number().optional().default(3.7),
// HSM
HSM_LIB_PATH: zpStr(z.string().optional()),
@@ -349,6 +351,8 @@ const envSchema = z
isTestMode: data.NODE_ENV === "test",
isRotationDevelopmentMode:
(data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE) || data.NODE_ENV === "test",
isDailyResourceCleanUpDevelopmentMode:
data.NODE_ENV === "development" && data.DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE,
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS),
REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim()
+14
View File
@@ -19,3 +19,17 @@ export const prefixWithSlash = (str: string) => {
const vowelRegex = new RE2(/^[aeiou]/i);
export const startsWithVowel = (str: string) => vowelRegex.test(str);
const pickWordsRegex = new RE2(/(\W+)/);
export const sanitizeString = (dto: { unsanitizedString: string; tokens: string[] }) => {
const words = dto.unsanitizedString.split(pickWordsRegex);
const redactionSet = new Set(dto.tokens.filter(Boolean));
const sanitizedWords = words.map((el) => {
if (redactionSet.has(el)) {
return "[REDACTED]";
}
return el;
});
return sanitizedWords.join("");
};
+3 -5
View File
@@ -45,6 +45,8 @@ import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal";
import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
@@ -179,8 +181,6 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { identityAliCloudAuthDALFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-dal";
import { identityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service";
import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal";
import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service";
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal";
@@ -849,8 +849,6 @@ export const registerRoutes = async (
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
projectBotDAL,
projectKeyDAL,
projectMembershipDAL
});
@@ -1974,7 +1972,7 @@ export const registerRoutes = async (
await telemetryQueue.startTelemetryCheck();
await telemetryQueue.startAggregatedEventsJob();
await dailyResourceCleanUp.startCleanUp();
await dailyResourceCleanUp.init();
await dailyReminderQueueService.startDailyRemindersJob();
await dailyReminderQueueService.startSecretReminderMigrationJob();
await dailyExpiringPkiItemAlert.startSendingAlerts();
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas";
import { RemindersSchema } from "@app/db/schemas/reminders";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -628,7 +629,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SanitizedTagSchema.array().optional()
tags: SanitizedTagSchema.array().optional(),
reminder: RemindersSchema.extend({
recipients: z.string().array().optional()
}).nullish()
})
.array()
.optional(),
@@ -706,7 +710,11 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImports>> | undefined;
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"] | undefined;
let secrets:
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & {
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
})[]
| undefined;
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
let secretRotations:
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
@@ -904,7 +912,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
});
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = (
const rawSecrets = (
await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
@@ -925,6 +933,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
includeMetadataInSearch: true
})
).secrets;
const reminders = await server.services.reminder.getRemindersForDashboard(
rawSecrets.map((secret) => secret.id)
);
secrets = rawSecrets.map((secret) => ({
...secret,
reminder: reminders[secret.id] ?? null
}));
}
}
} catch (error) {
@@ -19,7 +19,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const data = await req.file({
limits: {
@@ -69,7 +69,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
mappingType: z.nativeEnum(VaultMappingType)
})
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.migration.importVaultData({
actorId: req.permission.id,
@@ -44,7 +44,8 @@ const getConnectionConfig = ({
? {
trustServerCertificate: !sslRejectUnauthorized,
encrypt: true,
cryptoCredentialsDetails: sslCertificate ? { ca: sslCertificate } : {}
cryptoCredentialsDetails: sslCertificate ? { ca: sslCertificate } : {},
servername: host
}
: { encrypt: false }
};
@@ -148,9 +148,15 @@ export const authLoginServiceFactory = ({
if (organizationId) {
const org = await orgDAL.findById(organizationId);
if (org && org.userTokenExpiration) {
tokenSessionExpiresIn = getMinExpiresIn(cfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
refreshTokenExpiresIn = org.userTokenExpiration;
if (org) {
await orgMembershipDAL.update(
{ userId: user.id, orgId: org.id },
{ lastLoginAuthMethod: authMethod, lastLoginTime: new Date() }
);
if (org.userTokenExpiration) {
tokenSessionExpiresIn = getMinExpiresIn(cfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
refreshTokenExpiresIn = org.userTokenExpiration;
}
}
}
@@ -818,7 +824,6 @@ export const authLoginServiceFactory = ({
}
}
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const isUserCompleted = user.isAccepted;
const providerAuthToken = crypto.jwt().sign(
{
@@ -829,7 +834,7 @@ export const authLoginServiceFactory = ({
isEmailVerified: user.isEmailVerified,
firstName: user.firstName,
lastName: user.lastName,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
hasExchangedPrivateKey: true,
authMethod,
isUserCompleted,
...(callbackPort
@@ -874,8 +879,7 @@ export const authLoginServiceFactory = ({
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc?.serverEncryptedPrivateKey)
throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." });
if (!userEnc) throw new BadRequestError({ message: "User encryption not found" });
const token = await generateUserTokens({
user: { ...userEnc, id: userEnc.userId },
@@ -2,6 +2,7 @@ import { z } from "zod";
import { isValidIp } from "@app/lib/ip";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TAltNameMapping, TAltNameType } from "@app/services/certificate/certificate-types";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
@@ -56,3 +57,19 @@ export const validateAltNamesField = z
message: "Each alt name must be a valid hostname, email address, IP address or URL"
}
);
export const validateAndMapAltNameType = (name: string): TAltNameMapping | null => {
if (isFQDN(name, { allow_wildcard: true, require_tld: false })) {
return { type: TAltNameType.DNS, value: name };
}
if (z.string().url().safeParse(name).success) {
return { type: TAltNameType.URL, value: name };
}
if (z.string().email().safeParse(name).success) {
return { type: TAltNameType.EMAIL, value: name };
}
if (isValidIp(name)) {
return { type: TAltNameType.IP, value: name };
}
return null;
};
@@ -1,7 +1,6 @@
/* eslint-disable no-bitwise */
import * as x509 from "@peculiar/x509";
import RE2 from "re2";
import { z } from "zod";
import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
@@ -9,7 +8,6 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
@@ -17,7 +15,8 @@ import {
CertExtendedKeyUsage,
CertKeyAlgorithm,
CertKeyUsage,
CertStatus
CertStatus,
TAltNameMapping
} from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -33,6 +32,7 @@ import {
keyAlgorithmToAlgCfg
} from "../certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
import { validateAndMapAltNameType } from "../certificate-authority-validators";
import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types";
type TInternalCertificateAuthorityFnsDeps = {
@@ -152,27 +152,15 @@ export const InternalCertificateAuthorityFns = ({
extensions.push(extendedKeyUsagesExtension);
}
let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = [];
let altNamesArray: TAltNameMapping[] = [];
if (subscriber.subjectAlternativeNames?.length) {
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
if (z.string().email().safeParse(altName).success) {
return { type: "email", value: altName };
const altNameType = validateAndMapAltNameType(altName);
if (!altNameType) {
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
}
if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) {
return { type: "dns", value: altName };
}
if (z.string().url().safeParse(altName).success) {
return { type: "url", value: altName };
}
if (z.string().ip().safeParse(altName).success) {
return { type: "ip", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
return altNameType;
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -426,27 +414,15 @@ export const InternalCertificateAuthorityFns = ({
);
}
let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = [];
let altNamesArray: TAltNameMapping[] = [];
if (altNames) {
altNamesArray = altNames.split(",").map((altName) => {
if (z.string().email().safeParse(altName).success) {
return { type: "email", value: altName };
const altNameType = validateAndMapAltNameType(altName);
if (!altNameType) {
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
}
if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) {
return { type: "dns", value: altName };
}
if (z.string().url().safeParse(altName).success) {
return { type: "url", value: altName };
}
if (z.string().ip().safeParse(altName).success) {
return { type: "ip", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
return altNameType;
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -2,7 +2,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
@@ -18,7 +17,6 @@ import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -34,7 +32,8 @@ import {
CertExtendedKeyUsageOIDToName,
CertKeyAlgorithm,
CertKeyUsage,
CertStatus
CertStatus,
TAltNameMapping
} from "../../certificate/certificate-types";
import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal";
import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns";
@@ -53,6 +52,7 @@ import {
} from "../certificate-authority-fns";
import { TCertificateAuthorityQueueFactory } from "../certificate-authority-queue";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
import { validateAndMapAltNameType } from "../certificate-authority-validators";
import { TInternalCertificateAuthorityDALFactory } from "./internal-certificate-authority-dal";
import {
TCreateCaDTO,
@@ -1364,34 +1364,18 @@ export const internalCertificateAuthorityServiceFactory = ({
);
}
let altNamesArray: {
type: "email" | "dns";
value: string;
}[] = [];
let altNamesArray: TAltNameMapping[] = [];
if (altNames) {
altNamesArray = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
// check if the altName is a valid email
if (z.string().email().safeParse(altName).success) {
return {
type: "email",
value: altName
};
.map((altName): TAltNameMapping => {
const altNameType = validateAndMapAltNameType(altName);
if (!altNameType) {
throw new Error(`Invalid altName: ${altName}`);
}
// check if the altName is a valid hostname
if (isFQDN(altName, { allow_wildcard: true })) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
return altNameType;
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -1766,34 +1750,22 @@ export const internalCertificateAuthorityServiceFactory = ({
}
let altNamesFromCsr: string = "";
let altNamesArray: {
type: "email" | "dns";
value: string;
}[] = [];
let altNamesArray: TAltNameMapping[] = [];
if (altNames) {
altNamesArray = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
// check if the altName is a valid email
if (z.string().email().safeParse(altName).success) {
return {
type: "email",
value: altName
};
.map((altName): TAltNameMapping => {
const altNameType = validateAndMapAltNameType(altName);
if (!altNameType) {
throw new Error(`Invalid altName: ${altName}`);
}
// check if the altName is a valid hostname
if (isFQDN(altName, { allow_wildcard: true })) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
return altNameType;
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
} else {
// attempt to read from CSR if altNames is not explicitly provided
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
@@ -1801,11 +1773,16 @@ export const internalCertificateAuthorityServiceFactory = ({
const sanNames = new x509.GeneralNames(sanExtension.value);
altNamesArray = sanNames.items
.filter((value) => value.type === "email" || value.type === "dns")
.map((name) => ({
type: name.type as "email" | "dns",
value: name.value
}));
.filter(
(value) => value.type === "email" || value.type === "dns" || value.type === "url" || value.type === "ip"
)
.map((name): TAltNameMapping => {
const altNameType = validateAndMapAltNameType(name.value);
if (!altNameType) {
throw new Error(`Invalid altName from CSR: ${name.value}`);
}
return altNameType;
});
altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
}
@@ -104,3 +104,14 @@ export type TGetCertificateCredentialsDTO = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export enum TAltNameType {
EMAIL = "email",
DNS = "dns",
IP = "ip",
URL = "url"
}
export type TAltNameMapping = {
type: TAltNameType;
value: string;
};
@@ -254,29 +254,26 @@ export const transformToInfisicalFormatNamespaceToProjects = (
let currentFolderId: string | undefined;
let currentPath = "";
if (path.includes("/")) {
const pathParts = path.split("/").filter(Boolean);
const pathParts = path.split("/").filter(Boolean);
const folderParts = pathParts;
const folderParts = pathParts;
// create nested folder structure for the entire path
for (const folderName of folderParts) {
currentPath = currentPath ? `${currentPath}/${folderName}` : folderName;
const folderKey = `${namespace}:${mount}:${currentPath}`;
// create nested folder structure for the entire path
for (const folderName of folderParts) {
currentPath = currentPath ? `${currentPath}/${folderName}` : folderName;
const folderKey = `${namespace}:${mount}:${currentPath}`;
if (!folderMap.has(folderKey)) {
const folderId = uuidv4();
folderMap.set(folderKey, folderId);
folders.push({
id: folderId,
name: folderName,
environmentId,
parentFolderId: currentFolderId || environmentId
});
currentFolderId = folderId;
} else {
currentFolderId = folderMap.get(folderKey)!;
}
if (!folderMap.has(folderKey)) {
const folderId = uuidv4();
folderMap.set(folderKey, folderId);
folders.push({
id: folderId,
name: folderName,
environmentId,
parentFolderId: currentFolderId || environmentId
});
currentFolderId = folderId;
} else {
currentFolderId = folderMap.get(folderKey)!;
}
}
@@ -38,7 +38,7 @@ type TIdentityAliCloudAuthServiceFactoryDep = {
TIdentityAliCloudAuthDALFactory,
"findOne" | "transaction" | "create" | "updateById" | "delete"
>;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "updateById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
@@ -64,6 +64,8 @@ export const identityAliCloudAuthServiceFactory = ({
identityId: identityAliCloudAuth.identityId
});
if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" });
const requestUrl = new URL("https://sts.aliyuncs.com");
for (const key of Object.keys(params)) {
@@ -87,6 +89,14 @@ export const identityAliCloudAuthServiceFactory = ({
// Generate the token
const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityAliCloudAuth.identityId,
@@ -36,7 +36,7 @@ import {
type TIdentityAwsAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
identityAwsAuthDAL: Pick<TIdentityAwsAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "updateById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
@@ -91,6 +91,7 @@ export const identityAwsAuthServiceFactory = ({
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAwsAuth.identityId });
if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" });
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
const body: string = Buffer.from(iamRequestBody, "base64").toString();
@@ -152,6 +153,14 @@ export const identityAwsAuthServiceFactory = ({
}
const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityAwsAuth.identityId,
@@ -33,7 +33,7 @@ type TIdentityAzureAuthServiceFactoryDep = {
TIdentityAzureAuthDALFactory,
"findOne" | "transaction" | "create" | "updateById" | "delete"
>;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "updateById">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -80,6 +80,14 @@ export const identityAzureAuthServiceFactory = ({
}
const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityAzureAuth.identityId,
@@ -31,7 +31,7 @@ import {
type TIdentityGcpAuthServiceFactoryDep = {
identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "updateById">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -119,6 +119,14 @@ export const identityGcpAuthServiceFactory = ({
}
const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityGcpAuth.identityId,
@@ -43,7 +43,7 @@ import {
type TIdentityJwtAuthServiceFactoryDep = {
identityJwtAuthDAL: TIdentityJwtAuthDALFactory;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "updateById">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -209,6 +209,14 @@ export const identityJwtAuthServiceFactory = ({
}
const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityJwtAuth.identityId,
@@ -49,7 +49,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
"create" | "findOne" | "transaction" | "updateById" | "delete"
>;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById" | "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -380,6 +380,14 @@ export const identityKubernetesAuthServiceFactory = ({
}
const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityKubernetesAuth.identityId,
@@ -44,7 +44,7 @@ type TIdentityLdapAuthServiceFactoryDep = {
TIdentityLdapAuthDALFactory,
"findOne" | "transaction" | "create" | "updateById" | "delete"
>;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "updateById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: TKmsServiceFactory;
@@ -144,6 +144,14 @@ export const identityLdapAuthServiceFactory = ({
}
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityLdapAuth.identityId,
@@ -36,7 +36,7 @@ import {
type TIdentityOciAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
identityOciAuthDAL: Pick<TIdentityOciAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "updateById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
@@ -57,6 +57,7 @@ export const identityOciAuthServiceFactory = ({
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityOciAuth.identityId });
if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" });
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
@@ -91,6 +92,14 @@ export const identityOciAuthServiceFactory = ({
// Generate the token
const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityOciAuth.identityId,
@@ -43,7 +43,7 @@ import {
type TIdentityOidcAuthServiceFactoryDep = {
identityOidcAuthDAL: TIdentityOidcAuthDALFactory;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "updateById">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -178,6 +178,14 @@ export const identityOidcAuthServiceFactory = ({
}
const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityOidcAuth.identityId,
@@ -30,7 +30,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = {
TIdentityTlsCertAuthDALFactory,
"findOne" | "transaction" | "create" | "updateById" | "delete"
>;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "updateById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -118,6 +118,14 @@ export const identityTlsCertAuthServiceFactory = ({
// Generate the token
const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityTlsCertAuth.identityId,
@@ -35,7 +35,7 @@ type TIdentityTokenAuthServiceFactoryDep = {
TIdentityTokenAuthDALFactory,
"transaction" | "create" | "findOne" | "updateById" | "delete"
>;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "updateById">;
identityAccessTokenDAL: Pick<
TIdentityAccessTokenDALFactory,
"create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete"
@@ -345,6 +345,14 @@ export const identityTokenAuthServiceFactory = ({
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => {
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityTokenAuth.identityId,
@@ -59,6 +59,11 @@ export const identityUaServiceFactory = ({
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
if (!identityMembershipOrg) {
throw new NotFoundError({
message: "No identity with the org membership was found"
});
}
checkIPAgainstBlocklist({
ipAddress: ip,
@@ -127,7 +132,14 @@ export const identityUaServiceFactory = ({
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
await identityOrgMembershipDAL.updateById(
identityMembershipOrg.id,
{
lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityUa.identityId,
@@ -254,6 +254,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("role").withSchema("paginatedIdentity"),
db.ref("roleId").withSchema("paginatedIdentity"),
db.ref("orgId").withSchema("paginatedIdentity"),
db.ref("lastLoginAuthMethod").withSchema("paginatedIdentity"),
db.ref("lastLoginTime").withSchema("paginatedIdentity"),
db.ref("createdAt").withSchema("paginatedIdentity"),
db.ref("updatedAt").withSchema("paginatedIdentity"),
db.ref("identityId").withSchema("paginatedIdentity").as("identityId"),
@@ -319,7 +321,9 @@ export const identityOrgDALFactory = (db: TDbClient) => {
ldapId,
tlsCertId,
createdAt,
updatedAt
updatedAt,
lastLoginAuthMethod,
lastLoginTime
}) => ({
role,
roleId,
@@ -328,6 +332,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
orgId,
createdAt,
updatedAt,
lastLoginAuthMethod,
lastLoginTime,
customRole: roleId
? {
id: crId,
@@ -497,6 +503,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("orgId").withSchema(TableName.IdentityOrgMembership),
db.ref("createdAt").withSchema(TableName.IdentityOrgMembership),
db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership),
db.ref("lastLoginAuthMethod").withSchema(TableName.IdentityOrgMembership),
db.ref("lastLoginTime").withSchema(TableName.IdentityOrgMembership),
db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("hasDeleteProtection").withSchema(TableName.Identity),
@@ -531,10 +539,10 @@ export const identityOrgDALFactory = (db: TDbClient) => {
} else if (orderBy === OrgIdentityOrderBy.Role) {
void query.orderByRaw(
`
CASE
WHEN ??.role = ?
THEN ??.slug
ELSE ??.role
CASE
WHEN ??.role = ?
THEN ??.slug
ELSE ??.role
END ?
`,
[
@@ -576,7 +584,9 @@ export const identityOrgDALFactory = (db: TDbClient) => {
tokenId,
ldapId,
createdAt,
updatedAt
updatedAt,
lastLoginTime,
lastLoginAuthMethod
}) => ({
role,
roleId,
@@ -586,6 +596,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
orgId,
createdAt,
updatedAt,
lastLoginTime,
lastLoginAuthMethod,
customRole: roleId
? {
id: crId,
@@ -6,8 +6,6 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
@@ -20,8 +18,6 @@ type TOrgAdminServiceFactoryDep = {
TProjectMembershipDALFactory,
"findOne" | "create" | "transaction" | "delete" | "findAllProjectMembers"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
smtpService: Pick<TSmtpService, "sendMail">;
};
@@ -32,8 +28,6 @@ export const orgAdminServiceFactory = ({
permissionService,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
projectBotDAL,
projectUserMembershipRoleDAL,
smtpService
}: TOrgAdminServiceFactoryDep) => {
@@ -119,28 +113,6 @@ export const orgAdminServiceFactory = ({
return { isExistingMember: true, membership: projectMembership };
}
// missing membership thus add admin back as admin to project
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new NotFoundError({
message: `Project owner of project with ID '${projectId}' not found`
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new NotFoundError({
message: `Project owner's latest key of project with ID '${projectId}' not found`
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new NotFoundError({
message: `Project bot for project with ID '${projectId}' not found`
});
}
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
const newProjectMembership = await projectMembershipDAL.create(
{
@@ -32,6 +32,8 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("isActive").withSchema(TableName.OrgMembership),
db.ref("lastLoginAuthMethod").withSchema(TableName.OrgMembership),
db.ref("lastLoginTime").withSchema(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
@@ -64,7 +66,9 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
role,
status,
isActive,
inviteEmail
inviteEmail,
lastLoginAuthMethod,
lastLoginTime
}) => ({
roleId,
orgId,
@@ -73,6 +77,8 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
status,
isActive,
inviteEmail,
lastLoginAuthMethod,
lastLoginTime,
user: {
id: userId,
email,
+2
View File
@@ -285,6 +285,8 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("isActive").withSchema(TableName.OrgMembership),
db.ref("lastLoginAuthMethod").withSchema(TableName.OrgMembership),
db.ref("lastLoginTime").withSchema(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
+26 -1
View File
@@ -124,10 +124,35 @@ export const reminderDALFactory = (db: TDbClient) => {
return reminders[0] || null;
};
const findSecretReminders = async (secretIds: string[], tx?: Knex) => {
const rawReminders = await (tx || db)(TableName.Reminder)
.whereIn(`${TableName.Reminder}.secretId`, secretIds)
.leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`)
.select(selectAllTableCols(TableName.Reminder))
.select(db.ref("userId").withSchema(TableName.ReminderRecipient));
const reminders = sqlNestRelationships({
data: rawReminders,
key: "id",
parentMapper: (el) => ({
_id: el.id,
...RemindersSchema.parse(el)
}),
childrenMapper: [
{
key: "userId",
label: "recipients" as const,
mapper: ({ userId }) => userId
}
]
});
return reminders;
};
return {
...reminderOrm,
findSecretDailyReminders,
findUpcomingReminders,
findSecretReminder
findSecretReminder,
findSecretReminders
};
};
@@ -372,6 +372,21 @@ export const reminderServiceFactory = ({
};
};
const getRemindersForDashboard: TReminderServiceFactory["getRemindersForDashboard"] = async (secretIds) => {
// scott we don't need to check permissions/secret existence because these are the
// secrets from the dashboard that have already gone through these checks
const reminders = await reminderDAL.findSecretReminders(secretIds);
const reminderMap: Record<string, (typeof reminders)[number]> = {};
reminders.forEach((reminder) => {
if (reminder.secretId) reminderMap[reminder.secretId] = reminder;
});
return reminderMap;
};
return {
createReminder,
getReminder,
@@ -379,6 +394,7 @@ export const reminderServiceFactory = ({
deleteReminder,
deleteReminderBySecretId,
batchCreateReminders,
createReminderInternal
createReminderInternal,
getRemindersForDashboard
};
};
@@ -103,4 +103,6 @@ export interface TReminderServiceFactory {
id: string;
created: boolean;
}>;
getRemindersForDashboard: (secretIds: string[]) => Promise<Record<string, TReminder & { recipients: string[] }>>;
}
@@ -1,5 +1,6 @@
import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
@@ -41,51 +42,50 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
serviceTokenService,
orgService
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets();
await secretSharingDAL.pruneExpiredSecretRequests();
await snapshotDAL.pruneExcessSnapshots();
await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions();
await serviceTokenService.notifyExpiringTokens();
await orgService.notifyInvitedUsers();
await auditLogDAL.pruneAuditLog();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
});
const appCfg = getConfig();
// we do a repeat cron job in utc timezone at 12 Midnight each day
const startCleanUp = async () => {
// TODO(akhilmhdh): remove later
await queueService.stopRepeatableJob(
QueueName.AuditLogPrune,
QueueJobs.AuditLogPrune,
{ pattern: "0 0 * * *", utc: true },
QueueName.AuditLogPrune // just a job id
);
// clear previous job
await queueService.stopRepeatableJob(
QueueName.DailyResourceCleanUp,
if (appCfg.isDailyResourceCleanUpDevelopmentMode) {
logger.warn("Daily Resource Clean Up is in development mode.");
}
const init = async () => {
await queueService.startPg<QueueName.DailyResourceCleanUp>(
QueueJobs.DailyResourceCleanUp,
{ pattern: "0 0 * * *", utc: true },
QueueName.DailyResourceCleanUp // just a job id
async () => {
try {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets();
await secretSharingDAL.pruneExpiredSecretRequests();
await snapshotDAL.pruneExcessSnapshots();
await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions();
await serviceTokenService.notifyExpiringTokens();
await orgService.notifyInvitedUsers();
await auditLogDAL.pruneAuditLog();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
} catch (error) {
logger.error(error, `${QueueName.DailyResourceCleanUp}: resource cleanup failed`);
throw error;
}
},
{
batchSize: 1,
workerCount: 1,
pollingIntervalSeconds: 1
}
);
await queueService.schedulePg(
QueueJobs.DailyResourceCleanUp,
appCfg.isDailyResourceCleanUpDevelopmentMode ? "*/5 * * * *" : "0 0 * * *",
undefined,
{ tz: "UTC" }
);
await queueService.queue(QueueName.DailyResourceCleanUp, QueueJobs.DailyResourceCleanUp, undefined, {
delay: 5000,
jobId: QueueName.DailyResourceCleanUp,
repeat: { pattern: "0 0 * * *", utc: true }
});
};
queueService.listen(QueueName.DailyResourceCleanUp, "failed", (_, err) => {
logger.error(err, `${QueueName.DailyResourceCleanUp}: resource cleanup failed`);
});
return {
startCleanUp
init
};
};