solved merge conflicts

This commit is contained in:
x
2025-04-28 18:51:20 -04:00
228 changed files with 5223 additions and 522 deletions
@@ -14,6 +14,7 @@ export enum AppConnection {
Windmill = "windmill",
Auth0 = "auth0",
HCVault = "hashicorp-vault",
LDAP = "ldap",
TeamCity = "teamcity"
}
@@ -46,6 +46,7 @@ import {
HumanitecConnectionMethod,
validateHumanitecConnectionCredentials
} from "./humanitec";
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import {
@@ -83,6 +84,7 @@ export const listAppConnectionOptions = () => {
getWindmillConnectionListItem(),
getAuth0ConnectionListItem(),
getHCVaultConnectionListItem(),
getLdapConnectionListItem(),
getTeamCityConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -149,6 +151,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
};
@@ -188,6 +191,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Client Credentials";
case HCVaultConnectionMethod.AppRole:
return "App Role";
case LdapConnectionMethod.SimpleBind:
return "Simple Bind";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -234,5 +239,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
[AppConnection.HCVault]: platformManagedCredentialsNotSupported,
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
};
@@ -16,5 +16,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Windmill]: "Windmill",
[AppConnection.Auth0]: "Auth0",
[AppConnection.HCVault]: "Hashicorp Vault",
[AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity"
};
@@ -45,6 +45,7 @@ import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault";
import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service";
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
@@ -80,6 +81,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
[AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
};
@@ -63,6 +63,12 @@ import {
THumanitecConnectionInput,
TValidateHumanitecConnectionCredentialsSchema
} from "./humanitec";
import {
TLdapConnection,
TLdapConnectionConfig,
TLdapConnectionInput,
TValidateLdapConnectionCredentialsSchema
} from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import {
TPostgresConnection,
@@ -110,6 +116,7 @@ export type TAppConnection = { id: string } & (
| TWindmillConnection
| TAuth0Connection
| THCVaultConnection
| TLdapConnection
| TTeamCityConnection
);
@@ -133,6 +140,7 @@ export type TAppConnectionInput = { id: string } & (
| TWindmillConnectionInput
| TAuth0ConnectionInput
| THCVaultConnectionInput
| TLdapConnectionInput
| TTeamCityConnectionInput
);
@@ -162,6 +170,7 @@ export type TAppConnectionConfig =
| TWindmillConnectionConfig
| TAuth0ConnectionConfig
| THCVaultConnectionConfig
| TLdapConnectionConfig
| TTeamCityConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
@@ -180,6 +189,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateWindmillConnectionCredentialsSchema
| TValidateAuth0ConnectionCredentialsSchema
| TValidateHCVaultConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
@@ -0,0 +1,4 @@
export * from "./ldap-connection-enums";
export * from "./ldap-connection-fns";
export * from "./ldap-connection-schemas";
export * from "./ldap-connection-types";
@@ -0,0 +1,7 @@
export enum LdapConnectionMethod {
SimpleBind = "simple-bind"
}
export enum LdapProvider {
ActiveDirectory = "active-directory"
}
@@ -0,0 +1,102 @@
import ldap from "ldapjs";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { LdapConnectionMethod } from "./ldap-connection-enums";
import { TLdapConnectionConfig } from "./ldap-connection-types";
export const getLdapConnectionListItem = () => {
return {
name: "LDAP" as const,
app: AppConnection.LDAP as const,
methods: Object.values(LdapConnectionMethod) as [LdapConnectionMethod.SimpleBind]
};
};
const LDAP_TIMEOUT = 15_000;
export const getLdapConnectionClient = async ({
url,
dn,
password,
sslCertificate,
sslRejectUnauthorized = true
}: TLdapConnectionConfig["credentials"]) => {
await blockLocalAndPrivateIpAddresses(url);
const isSSL = url.startsWith("ldaps");
return new Promise<ldap.Client>((resolve, reject) => {
const client = ldap.createClient({
url,
timeout: LDAP_TIMEOUT,
connectTimeout: LDAP_TIMEOUT,
tlsOptions: isSSL
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate ? [sslCertificate] : undefined
}
: undefined
});
client.on("error", (err: Error) => {
logger.error(err, "LDAP Error");
client.destroy();
reject(new Error(`Provider Error - ${err.message}`));
});
client.on("connectError", (err: Error) => {
logger.error(err, "LDAP Connection Error");
client.destroy();
reject(new Error(`Provider Connect Error - ${err.message}`));
});
client.on("connectRefused", (err: Error) => {
logger.error(err, "LDAP Connection Refused");
client.destroy();
reject(new Error(`Provider Connection Refused - ${err.message}`));
});
client.on("connectTimeout", (err: Error) => {
logger.error(err, "LDAP Connection Timeout");
client.destroy();
reject(new Error(`Provider Connection Timeout - ${err.message}`));
});
client.on("connect", () => {
client.bind(dn, password, (err) => {
if (err) {
logger.error(err, "LDAP Bind Error");
reject(new Error(`Bind Error: ${err.message}`));
client.destroy();
}
resolve(client);
});
});
});
};
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
let client: ldap.Client | undefined;
try {
client = await getLdapConnectionClient(credentials);
// this shouldn't occur as handle connection error events in client but here as fallback
if (!client.connected) {
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
}
return credentials;
} catch (e: unknown) {
throw new BadRequestError({
message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}`
});
} finally {
client?.destroy();
}
};
@@ -0,0 +1,93 @@
import RE2 from "re2";
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { DistinguishedNameRegex } from "@app/lib/regex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
export const LdapConnectionSimpleBindCredentialsSchema = z.object({
provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider),
url: z
.string()
.trim()
.min(1, "URL required")
.regex(new RE2(/^ldaps?:\/\//))
.describe(AppConnections.CREDENTIALS.LDAP.url),
dn: z
.string()
.trim()
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
.min(1, "Distinguished Name (DN) required")
.describe(AppConnections.CREDENTIALS.LDAP.dn),
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password),
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized),
sslCertificate: z
.string()
.trim()
.transform((value) => value || undefined)
.optional()
.describe(AppConnections.CREDENTIALS.LDAP.sslCertificate)
});
const BaseLdapConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.LDAP)
});
export const LdapConnectionSchema = z.intersection(
BaseLdapConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(LdapConnectionMethod.SimpleBind),
credentials: LdapConnectionSimpleBindCredentialsSchema
})
])
);
export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [
BaseLdapConnectionSchema.extend({
method: z.literal(LdapConnectionMethod.SimpleBind),
credentials: LdapConnectionSimpleBindCredentialsSchema.pick({
provider: true,
url: true,
dn: true,
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);
export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(LdapConnectionMethod.SimpleBind).describe(AppConnections.CREATE(AppConnection.LDAP).method),
credentials: LdapConnectionSimpleBindCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.LDAP).credentials
)
})
]);
export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP)
);
export const UpdateLdapConnectionSchema = z
.object({
credentials: LdapConnectionSimpleBindCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.LDAP).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP));
export const LdapConnectionListItemSchema = z.object({
name: z.literal("LDAP"),
app: z.literal(AppConnection.LDAP),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(LdapConnectionMethod).array()
});
@@ -0,0 +1,22 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateLdapConnectionSchema,
LdapConnectionSchema,
ValidateLdapConnectionCredentialsSchema
} from "./ldap-connection-schemas";
export type TLdapConnection = z.infer<typeof LdapConnectionSchema>;
export type TLdapConnectionInput = z.infer<typeof CreateLdapConnectionSchema> & {
app: AppConnection.LDAP;
};
export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema;
export type TLdapConnectionConfig = DiscriminativePick<TLdapConnection, "method" | "app" | "credentials"> & {
orgId: string;
};
@@ -31,7 +31,8 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
port: true,
username: true,
sslEnabled: true,
sslRejectUnauthorized: true
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);
@@ -29,7 +29,8 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method",
port: true,
username: true,
sslEnabled: true,
sslRejectUnauthorized: true
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);
@@ -476,6 +476,7 @@ export const authLoginServiceFactory = ({
return {
...tokens,
user,
isMfaEnabled: false
};
};
@@ -784,7 +785,7 @@ export const authLoginServiceFactory = ({
organizationId
});
return { token, isMfaEnabled: false, user: userEnc } as const;
return { token, isMfaEnabled: false, user: userEnc, decodedProviderToken } as const;
};
/*
@@ -177,6 +177,7 @@ export const deleteGithubSecrets = async ({
selected_repositories_url?: string | undefined;
}
// @ts-expect-error just octokit ts compatiability issue
const OctokitWithRetry = Octokit.plugin(retry);
let octokit: Octokit;
const appCfg = getConfig();
+12 -6
View File
@@ -342,9 +342,12 @@ export const kmsServiceFactory = ({
}
return async ({ cipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
const { data } = await externalKms.decrypt(cipherTextBlob);
return data;
try {
const { data } = await externalKms.decrypt(cipherTextBlob);
return data;
} finally {
await externalKms.cleanup();
}
};
}
@@ -557,9 +560,12 @@ export const kmsServiceFactory = ({
}
return async ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
const { encryptedBlob } = await externalKms.encrypt(plainText);
return { cipherTextBlob: encryptedBlob };
try {
const { encryptedBlob } = await externalKms.encrypt(plainText);
return { cipherTextBlob: encryptedBlob };
} finally {
await externalKms.cleanup();
}
};
}
@@ -23,6 +23,7 @@ import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectMembershipDALFactory } from "./project-membership-dal";
@@ -53,6 +54,7 @@ type TProjectMembershipServiceFactoryDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
groupProjectDAL: TGroupProjectDALFactory;
};
@@ -71,6 +73,7 @@ export const projectMembershipServiceFactory = ({
groupProjectDAL,
projectDAL,
projectKeyDAL,
secretReminderRecipientsDAL,
licenseService
}: TProjectMembershipServiceFactoryDep) => {
const getProjectMemberships = async ({
@@ -389,6 +392,13 @@ export const projectMembershipServiceFactory = ({
const membership = await projectMembershipDAL.transaction(async (tx) => {
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
await secretReminderRecipientsDAL.delete(
{
projectId,
userId: deletedMembership.userId
},
tx
);
return deletedMembership;
});
return membership;
@@ -466,6 +476,16 @@ export const projectMembershipServiceFactory = ({
tx
);
await secretReminderRecipientsDAL.delete(
{
projectId,
$in: {
userId: projectMembers.map(({ user }) => user.id)
}
},
tx
);
// delete project keys belonging to users that are not part of any other groups in the project
await projectKeyDAL.delete(
{
@@ -526,6 +546,15 @@ export const projectMembershipServiceFactory = ({
},
tx
);
await secretReminderRecipientsDAL.delete(
{
projectId,
userId: actorId
},
tx
);
const membership = (
await projectMembershipDAL.delete(
{
@@ -1,5 +1,6 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { requestContext } from "@fastify/request-context";
import { ActionProjectType, ProjectMembershipRole, TableName } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -12,10 +13,12 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorAuthMethod } from "../auth/auth-type";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectRoleDALFactory } from "./project-role-dal";
import { getPredefinedRoles } from "./project-role-fns";
import {
@@ -29,6 +32,8 @@ import {
type TProjectRoleServiceFactoryDep = {
projectRoleDAL: TProjectRoleDALFactory;
identityDAL: Pick<TIdentityDALFactory, "findById">;
userDAL: Pick<TUserDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
@@ -47,7 +52,9 @@ export const projectRoleServiceFactory = ({
permissionService,
identityProjectMembershipRoleDAL,
projectUserMembershipRoleDAL,
projectDAL
projectDAL,
identityDAL,
userDAL
}: TProjectRoleServiceFactoryDep) => {
const createRole = async ({ data, actor, actorId, actorAuthMethod, actorOrgId, filter }: TCreateRoleDTO) => {
let projectId = "";
@@ -220,14 +227,42 @@ export const projectRoleServiceFactory = ({
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission, membership } = await permissionService.getUserProjectPermission({
userId,
const { permission, membership } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId: userId,
projectId,
authMethod: actorAuthMethod,
userOrgId: actorOrgId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
return { permissions: packRules(permission.rules), membership };
// just to satisfy ts
if (!("roles" in membership)) throw new BadRequestError({ message: "Service token not allowed" });
const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails");
const isAssumingPrivilege = assumedPrivilegeDetailsCtx?.projectId === projectId;
const assumedPrivilegeDetails = isAssumingPrivilege
? {
actorId: assumedPrivilegeDetailsCtx?.actorId,
actorType: assumedPrivilegeDetailsCtx?.actorType,
actorName: "",
actorEmail: ""
}
: undefined;
if (assumedPrivilegeDetails?.actorType === ActorType.IDENTITY) {
const identityDetails = await identityDAL.findById(assumedPrivilegeDetails.actorId);
if (!identityDetails)
throw new NotFoundError({ message: `Identity with ID ${assumedPrivilegeDetails.actorId} not found` });
assumedPrivilegeDetails.actorName = identityDetails.name;
} else if (assumedPrivilegeDetails?.actorType === ActorType.USER) {
const userDetails = await userDAL.findById(assumedPrivilegeDetails?.actorId);
if (!userDetails)
throw new NotFoundError({ message: `User with ID ${assumedPrivilegeDetails.actorId} not found` });
assumedPrivilegeDetails.actorName = `${userDetails?.firstName} ${userDetails?.lastName || ""}`;
assumedPrivilegeDetails.actorEmail = userDetails?.email || "";
}
return { permissions: packRules(permission.rules), membership, assumedPrivilegeDetails };
};
return { createRole, updateRole, deleteRole, listRoles, getUserPermission, getRoleBySlug };
@@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSecretReminderRecipientsDALFactory = ReturnType<typeof secretReminderRecipientsDALFactory>;
export const secretReminderRecipientsDALFactory = (db: TDbClient) => {
const secretReminderRecipientsOrm = ormify(db, TableName.SecretReminderRecipients);
const findUsersBySecretId = async (secretId: string, tx?: Knex) => {
const res = await (tx || db.replicaNode())(TableName.SecretReminderRecipients)
.where({ secretId })
.leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.Project, `${TableName.SecretReminderRecipients}.projectId`, `${TableName.Project}.id`)
.leftJoin(TableName.OrgMembership, (bd) => {
void bd
.on(`${TableName.OrgMembership}.userId`, "=", `${TableName.SecretReminderRecipients}.userId`)
.andOn(`${TableName.OrgMembership}.orgId`, "=", `${TableName.Project}.orgId`);
})
.where(`${TableName.OrgMembership}.isActive`, true)
.select(selectAllTableCols(TableName.SecretReminderRecipients))
.select(
db.ref("email").withSchema(TableName.Users).as("email"),
db.ref("username").withSchema(TableName.Users).as("username"),
db.ref("firstName").withSchema(TableName.Users).as("firstName"),
db.ref("lastName").withSchema(TableName.Users).as("lastName")
);
return res;
};
return { ...secretReminderRecipientsOrm, findUsersBySecretId };
};
@@ -0,0 +1,8 @@
export type TSecretReminderRecipient = {
user: {
id: string;
username: string;
email?: string | null;
};
id: string;
};
@@ -7,6 +7,7 @@ import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecre
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { generateCacheKeyFromData } from "@app/lib/crypto/cache";
import { applyJitter } from "@app/lib/dates";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import {
buildFindFilter,
@@ -48,7 +49,7 @@ interface TSecretV2DalArg {
keyStore: TKeyStoreFactory;
}
export const SECRET_DAL_TTL = 5 * 60;
export const SECRET_DAL_TTL = () => applyJitter(10 * 60, 2 * 60);
export const SECRET_DAL_VERSION_TTL = 15 * 60;
export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024;
export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
@@ -79,7 +80,17 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
`${TableName.SecretV2}.id`,
`${TableName.SecretRotationV2SecretMapping}.secretId`
)
.leftJoin(
TableName.SecretReminderRecipients,
`${TableName.SecretV2}.id`,
`${TableName.SecretReminderRecipients}.secretId`
)
.leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientId"))
.select(db.ref("username").withSchema(TableName.Users).as("reminderRecipientUsername"))
.select(db.ref("email").withSchema(TableName.Users).as("reminderRecipientEmail"))
.select(db.ref("id").withSchema(TableName.Users).as("reminderRecipientUserId"))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
@@ -103,6 +114,23 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
slug,
name: slug
})
},
{
key: "reminderRecipientId",
label: "secretReminderRecipients" as const,
mapper: ({
reminderRecipientId,
reminderRecipientUsername,
reminderRecipientEmail,
reminderRecipientUserId
}) => ({
user: {
id: reminderRecipientUserId,
username: reminderRecipientUsername,
email: reminderRecipientEmail
},
id: reminderRecipientId
})
}
]
});
@@ -484,6 +512,12 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(
TableName.SecretReminderRecipients,
`${TableName.SecretV2}.id`,
`${TableName.SecretReminderRecipients}.secretId`
)
.leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.leftJoin(
TableName.SecretRotationV2SecretMapping,
@@ -512,6 +546,10 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
}) as rank`
)
)
.select(db.ref("id").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientId"))
.select(db.ref("username").withSchema(TableName.Users).as("reminderRecipientUsername"))
.select(db.ref("email").withSchema(TableName.Users).as("reminderRecipientEmail"))
.select(db.ref("id").withSchema(TableName.Users).as("reminderRecipientUserId"))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
@@ -556,6 +594,23 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
isRotatedSecret: Boolean(el.rotationId)
}),
childrenMapper: [
{
key: "reminderRecipientId",
label: "secretReminderRecipients" as const,
mapper: ({
reminderRecipientId,
reminderRecipientUsername,
reminderRecipientEmail,
reminderRecipientUserId
}) => ({
user: {
id: reminderRecipientUserId,
username: reminderRecipientUsername,
email: reminderRecipientEmail
},
id: reminderRecipientId
})
},
{
key: "tagId",
label: "tags" as const,
@@ -12,6 +12,7 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretReminderRecipient } from "../secret-reminder-recipients/secret-reminder-recipients-types";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
@@ -353,7 +354,7 @@ export const fnSecretBulkDelete = async ({
deletedSecrets
.filter(({ reminderRepeatDays }) => Boolean(reminderRepeatDays))
.map(({ id, reminderRepeatDays }) =>
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: reminderRepeatDays as number })
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: reminderRepeatDays as number }, tx)
)
);
@@ -684,6 +685,7 @@ export const reshapeBridgeSecret = (
secretMetadata?: ResourceMetadataDTO;
isRotatedSecret?: boolean;
rotationId?: string;
secretReminderRecipients?: TSecretReminderRecipient[];
},
secretValueHidden: boolean
) => ({
@@ -715,6 +717,7 @@ export const reshapeBridgeSecret = (
updatedAt: secret.updatedAt,
isRotatedSecret: secret.isRotatedSecret,
rotationId: secret.rotationId,
secretReminderRecipients: secret.secretReminderRecipients || [],
...(secretValueHidden
? {
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
@@ -544,7 +544,12 @@ export const secretV2BridgeServiceFactory = ({
id: updatedSecret[0].id,
...inputSecret
},
oldSecret: secret,
oldSecret: {
id: secret.id,
secretReminderNote: secret.reminderNote,
secretReminderRepeatDays: secret.reminderRepeatDays,
secretReminderRecipients: secret.secretReminderRecipients?.map((el) => el.user.id)
},
projectId
});
@@ -957,7 +962,7 @@ export const secretV2BridgeServiceFactory = ({
const encryptedCachedSecrets = await keyStore.getItem(cacheKey);
if (encryptedCachedSecrets) {
try {
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL());
const cachedSecrets = secretManagerDecryptor({ cipherTextBlob: Buffer.from(encryptedCachedSecrets, "base64") });
const { secrets, imports = [] } = JSON.parse(cachedSecrets.toString("utf8")) as {
secrets: typeof decryptedSecrets;
@@ -1127,7 +1132,7 @@ export const secretV2BridgeServiceFactory = ({
plainText: Buffer.from(JSON.stringify(payload))
}).cipherTextBlob;
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64"));
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL(), encryptedUpdatedCachedSecrets.toString("base64"));
}
return payload;
}
@@ -1174,7 +1179,7 @@ export const secretV2BridgeServiceFactory = ({
plainText: Buffer.from(JSON.stringify(payload))
}).cipherTextBlob;
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64"));
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL(), encryptedUpdatedCachedSecrets.toString("base64"));
}
return payload;
};
@@ -94,6 +94,7 @@ export type TUpdateSecretDTO = TProjectPermission & {
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretReminderRecipients?: string[] | null;
metadata?: {
source?: string;
};
@@ -220,7 +221,7 @@ export type TFnSecretBulkDelete = {
tx?: Knex;
secretDAL: Pick<TSecretV2BridgeDALFactory, "deleteMany">;
secretQueueService: {
removeSecretReminder: (data: TRemoveSecretReminderDTO) => Promise<void>;
removeSecretReminder: (data: TRemoveSecretReminderDTO, tx?: Knex) => Promise<void>;
};
};
+2 -1
View File
@@ -407,6 +407,7 @@ export const decryptSecretRaw = (
id: secret.id,
user: secret.userId,
tags: secret.tags?.map((el) => ({ ...el, name: el.slug })),
secretReminderRecipients: [],
skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.secretReminderRepeatDays,
secretReminderNote: secret.secretReminderNote,
@@ -758,7 +759,7 @@ export const fnSecretBulkDelete = async ({
deletedSecrets
.filter(({ secretReminderRepeatDays }) => Boolean(secretReminderRepeatDays))
.map(({ id, secretReminderRepeatDays }) =>
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: secretReminderRepeatDays as number })
secretQueueService.removeSecretReminder({ secretId: id, repeatDays: secretReminderRepeatDays as number }, tx)
)
);
+77 -25
View File
@@ -1,11 +1,13 @@
/* eslint-disable no-await-in-loop */
import opentelemetry from "@opentelemetry/api";
import { AxiosError } from "axios";
import { Knex } from "knex";
import {
ProjectMembershipRole,
ProjectUpgradeStatus,
ProjectVersion,
SecretType,
TSecretSnapshotSecretsV2,
TSecretVersionsV2
} from "@app/db/schemas";
@@ -53,6 +55,7 @@ import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-sche
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
@@ -109,6 +112,10 @@ type TSecretQueueFactoryDep = {
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretReminderRecipientsDAL: Pick<
TSecretReminderRecipientsDALFactory,
"delete" | "findUsersBySecretId" | "insertMany" | "transaction"
>;
secretSyncQueue: Pick<TSecretSyncQueueFactory, "queueSecretSyncsSyncSecretsByPath">;
};
@@ -170,6 +177,7 @@ export const secretQueueFactory = ({
projectUserMembershipRoleDAL,
projectKeyDAL,
resourceMetadataDAL,
secretReminderRecipientsDAL,
secretSyncQueue
}: TSecretQueueFactoryDep) => {
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
@@ -178,7 +186,11 @@ export const secretQueueFactory = ({
unit: "1"
});
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
const removeSecretReminder = async ({ deleteRecipients = true, ...dto }: TRemoveSecretReminderDTO, tx?: Knex) => {
if (deleteRecipients) {
await secretReminderRecipientsDAL.delete({ secretId: dto.secretId }, tx);
}
const appCfg = getConfig();
await queueService.stopRepeatableJob(
QueueName.SecretReminder,
@@ -224,7 +236,12 @@ export const secretQueueFactory = ({
.replace(":", "-");
};
const addSecretReminder = async ({ oldSecret, newSecret, projectId }: TCreateSecretReminderDTO) => {
const addSecretReminder = async ({
oldSecret,
newSecret,
projectId,
deleteRecipients = true
}: TCreateSecretReminderDTO) => {
try {
const appCfg = getConfig();
@@ -246,7 +263,8 @@ export const secretQueueFactory = ({
if (oldSecret.secretReminderRepeatDays) {
await removeSecretReminder({
repeatDays: oldSecret.secretReminderRepeatDays,
secretId: oldSecret.id
secretId: oldSecret.id,
deleteRecipients
});
}
@@ -283,29 +301,57 @@ export const secretQueueFactory = ({
};
const handleSecretReminder = async ({ newSecret, oldSecret, projectId }: THandleReminderDTO) => {
const { secretReminderRepeatDays, secretReminderNote } = newSecret;
const { secretReminderRepeatDays, secretReminderNote, secretReminderRecipients } = newSecret;
if (newSecret.type !== "personal" && secretReminderRepeatDays !== undefined) {
if (
(secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) ||
(secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote)
) {
await addSecretReminder({
oldSecret,
newSecret,
projectId
});
} else if (
secretReminderRepeatDays === null &&
secretReminderNote === null &&
oldSecret.secretReminderRepeatDays
) {
await removeSecretReminder({
secretId: oldSecret.id,
repeatDays: oldSecret.secretReminderRepeatDays
});
const recipientsUpdated =
secretReminderRecipients?.some(
(newId) => !oldSecret.secretReminderRecipients?.find((oldId) => newId === oldId)
) || secretReminderRecipients?.length !== oldSecret.secretReminderRecipients?.length;
await secretReminderRecipientsDAL.transaction(async (tx) => {
if (newSecret.type !== SecretType.Personal && secretReminderRepeatDays !== undefined) {
if (
(secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) ||
(secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote)
) {
await addSecretReminder({
oldSecret,
newSecret,
projectId,
deleteRecipients: false
});
} else if (
secretReminderRepeatDays === null &&
secretReminderNote === null &&
oldSecret.secretReminderRepeatDays
) {
await removeSecretReminder({
secretId: oldSecret.id,
repeatDays: oldSecret.secretReminderRepeatDays
});
}
}
}
if (recipientsUpdated) {
// if no recipients, delete all existing recipients
if (!secretReminderRecipients?.length) {
const existingRecipients = await secretReminderRecipientsDAL.findUsersBySecretId(newSecret.id, tx);
if (existingRecipients) {
await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx);
}
} else {
await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx);
await secretReminderRecipientsDAL.insertMany(
secretReminderRecipients.map((r) => ({
secretId: newSecret.id,
userId: r,
projectId
})),
tx
);
}
}
});
};
const createManySecretsRawFn = createManySecretsRawFnFactory({
projectDAL,
@@ -1071,6 +1117,8 @@ export const secretQueueFactory = ({
const secret = await secretV2BridgeDAL.findById(data.secretId);
const [folder] = await folderDAL.findSecretPathByFolderIds(project.id, [secret.folderId]);
const recipients = await secretReminderRecipientsDAL.findUsersBySecretId(data.secretId);
if (!organization) {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`);
return;
@@ -1088,10 +1136,14 @@ export const secretQueueFactory = ({
return;
}
const selectedRecipients = recipients?.length
? recipients.map((r) => r.email as string)
: projectMembers.map((m) => m.user.email as string);
await smtpService.sendMail({
template: SmtpTemplates.SecretReminder,
subjectLine: "Infisical secret reminder",
recipients: [...projectMembers.map((m) => m.user.email)].filter((email) => email).map((email) => email as string),
recipients: selectedRecipients,
substitutions: {
reminderNote: data.note, // May not be present.
projectName: project.name,
+19 -9
View File
@@ -546,10 +546,13 @@ export const secretServiceFactory = ({
for await (const secret of secrets) {
if (secret.secretReminderRepeatDays !== null && secret.secretReminderRepeatDays !== undefined) {
await secretQueueService.removeSecretReminder({
repeatDays: secret.secretReminderRepeatDays,
secretId: secret.id
});
await secretQueueService.removeSecretReminder(
{
repeatDays: secret.secretReminderRepeatDays,
secretId: secret.id
},
tx
);
}
}
@@ -685,6 +688,7 @@ export const secretServiceFactory = ({
...secret,
workspace: projectId,
environment,
secretReminderRecipients: [],
secretPath: groupedPaths[secret.folderId][0].path
}))
};
@@ -1073,10 +1077,13 @@ export const secretServiceFactory = ({
for await (const secret of secrets) {
if (secret.secretReminderRepeatDays !== null && secret.secretReminderRepeatDays !== undefined) {
await secretQueueService.removeSecretReminder({
repeatDays: secret.secretReminderRepeatDays,
secretId: secret.id
});
await secretQueueService.removeSecretReminder(
{
repeatDays: secret.secretReminderRepeatDays,
secretId: secret.id
},
tx
);
}
}
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
@@ -1786,6 +1793,7 @@ export const secretServiceFactory = ({
tagIds,
secretReminderNote,
secretReminderRepeatDays,
secretReminderRecipients,
metadata,
secretComment,
newSecretName,
@@ -1828,6 +1836,7 @@ export const secretServiceFactory = ({
tagIds,
reminderNote: secretReminderNote,
reminderRepeatDays: secretReminderRepeatDays,
secretReminderRecipients,
secretMetadata
}
]
@@ -1837,8 +1846,9 @@ export const secretServiceFactory = ({
}
const secret = await secretV2BridgeService.updateSecret({
secretReminderRepeatDays,
skipMultilineEncoding,
secretReminderNote,
secretReminderRecipients,
skipMultilineEncoding,
tagIds,
secretComment,
secretPath,
+11 -3
View File
@@ -22,9 +22,13 @@ import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">;
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote"> & {
secretReminderRecipients?: string[] | null;
};
type TPartialInputSecret = Pick<TSecrets, "type" | "secretReminderNote" | "secretReminderRepeatDays" | "id">;
type TPartialInputSecret = Pick<TSecrets, "type" | "secretReminderNote" | "secretReminderRepeatDays" | "id"> & {
secretReminderRecipients?: string[] | null;
};
export const FailedIntegrationSyncEmailsPayloadSchema = z.object({
projectId: z.string(),
@@ -258,6 +262,7 @@ export type TUpdateSecretRawDTO = TProjectPermission & {
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretReminderRecipients?: string[] | null;
metadata?: {
source?: string;
};
@@ -374,7 +379,7 @@ export type TFnSecretBulkDelete = {
tx?: Knex;
secretDAL: Pick<TSecretDALFactory, "deleteMany">;
secretQueueService: {
removeSecretReminder: (data: TRemoveSecretReminderDTO) => Promise<void>;
removeSecretReminder: (data: TRemoveSecretReminderDTO, tx?: Knex) => Promise<void>;
};
};
@@ -405,11 +410,14 @@ export type TCreateSecretReminderDTO = {
oldSecret: TPartialSecret;
newSecret: TPartialSecret;
projectId: string;
deleteRecipients?: boolean;
};
export type TRemoveSecretReminderDTO = {
secretId: string;
repeatDays: number;
deleteRecipients?: boolean;
};
export type TBackFillSecretReferencesDTO = TProjectPermission;