mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 09:35:37 +00:00
Merge remote-tracking branch 'origin/main' into misc/terraform-project-group-prereq
This commit is contained in:
@@ -34,6 +34,7 @@ export enum AuthMode {
|
||||
}
|
||||
|
||||
export enum ActorType { // would extend to AWS, Azure, ...
|
||||
PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs.
|
||||
USER = "user", // userIdentity
|
||||
SERVICE = "service",
|
||||
IDENTITY = "identity",
|
||||
|
||||
@@ -157,7 +157,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
`${TableName.ProjectRoles}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembership),
|
||||
db.ref("id").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("isGhost").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
infisicalSymmetricDecrypt,
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
@@ -68,12 +68,12 @@ export const identityOidcAuthServiceFactory = ({
|
||||
identityId: identityOidcAuth.identityId
|
||||
});
|
||||
if (!identityMembershipOrg) {
|
||||
throw new BadRequestError({ message: "Failed to find identity" });
|
||||
throw new NotFoundError({ message: "Failed to find identity in organization" });
|
||||
}
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
||||
if (!orgBot) {
|
||||
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
throw new NotFoundError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||
}
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
@@ -106,7 +106,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
|
||||
const decodedToken = jwt.decode(oidcJwt, { complete: true });
|
||||
if (!decodedToken) {
|
||||
throw new BadRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid JWT"
|
||||
});
|
||||
}
|
||||
@@ -119,13 +119,24 @@ export const identityOidcAuthServiceFactory = ({
|
||||
const { kid } = decodedToken.header;
|
||||
const oidcSigningKey = await client.getSigningKey(kid);
|
||||
|
||||
const tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), {
|
||||
issuer: identityOidcAuth.boundIssuer
|
||||
}) as Record<string, string>;
|
||||
let tokenData: Record<string, string>;
|
||||
try {
|
||||
tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), {
|
||||
issuer: identityOidcAuth.boundIssuer
|
||||
}) as Record<string, string>;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (identityOidcAuth.boundSubject) {
|
||||
if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) {
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: OIDC subject not allowed."
|
||||
});
|
||||
}
|
||||
@@ -137,7 +148,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
.split(", ")
|
||||
.some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue))
|
||||
) {
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: OIDC audience not allowed."
|
||||
});
|
||||
}
|
||||
@@ -150,7 +161,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
if (
|
||||
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
|
||||
) {
|
||||
throw new ForbiddenRequestError({
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: OIDC claim not allowed."
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +58,8 @@ export const identityServiceFactory = ({
|
||||
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged identity" });
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
|
||||
|
||||
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
|
||||
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create identity due to identity limit reached. Upgrade plan to create more identities."
|
||||
|
||||
@@ -242,37 +242,12 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
||||
};
|
||||
}
|
||||
|
||||
const octokit = new Octokit({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const repos = (await new Octokit({
|
||||
auth: accessToken
|
||||
});
|
||||
|
||||
const getAllRepos = async () => {
|
||||
let repos: GitHubApp[] = [];
|
||||
let page = 1;
|
||||
const perPage = 100;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await octokit.request(
|
||||
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
|
||||
{
|
||||
per_page: perPage,
|
||||
page
|
||||
}
|
||||
);
|
||||
|
||||
if ((response.data as GitHubApp[]).length > 0) {
|
||||
repos = repos.concat(response.data as GitHubApp[]);
|
||||
page += 1;
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
return repos;
|
||||
};
|
||||
|
||||
const repos = await getAllRepos();
|
||||
}).paginate("GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", {
|
||||
per_page: 100
|
||||
})) as GitHubApp[];
|
||||
|
||||
const apps = repos
|
||||
.filter((a: GitHubApp) => a.permissions.admin === true)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||
@@ -19,6 +19,7 @@ import { TIntegrationDALFactory } from "./integration-dal";
|
||||
import {
|
||||
TCreateIntegrationDTO,
|
||||
TDeleteIntegrationDTO,
|
||||
TGetIntegrationDTO,
|
||||
TSyncIntegrationDTO,
|
||||
TUpdateIntegrationDTO
|
||||
} from "./integration-types";
|
||||
@@ -180,6 +181,27 @@ export const integrationServiceFactory = ({
|
||||
return updatedIntegration;
|
||||
};
|
||||
|
||||
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
|
||||
const integration = await integrationDAL.findById(id);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integration?.projectId || "",
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
|
||||
if (!integration) {
|
||||
throw new NotFoundError({
|
||||
message: "Integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
return { ...integration, envId: integration.environment.id };
|
||||
};
|
||||
|
||||
const deleteIntegration = async ({
|
||||
actorId,
|
||||
id,
|
||||
@@ -276,6 +298,8 @@ export const integrationServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
|
||||
await secretQueueService.syncIntegrations({
|
||||
isManual: true,
|
||||
actorId,
|
||||
environment: integration.environment.slug,
|
||||
secretPath: integration.secretPath,
|
||||
projectId: integration.projectId
|
||||
@@ -289,6 +313,7 @@ export const integrationServiceFactory = ({
|
||||
updateIntegration,
|
||||
deleteIntegration,
|
||||
listIntegrationByProject,
|
||||
getIntegration,
|
||||
syncIntegration
|
||||
};
|
||||
};
|
||||
|
||||
@@ -39,6 +39,10 @@ export type TCreateIntegrationDTO = {
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetIntegrationDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateIntegrationDTO = {
|
||||
id: string;
|
||||
app?: string;
|
||||
|
||||
@@ -60,7 +60,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
||||
name: "Admin",
|
||||
slug: "admin",
|
||||
description: "Complete administration access over the organization",
|
||||
permissions: packRules(orgAdminPermissions.rules),
|
||||
permissions: packRules(orgAdminPermissions),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
@@ -72,7 +72,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
||||
name: "Member",
|
||||
slug: "member",
|
||||
description: "Non-administrative role in an organization",
|
||||
permissions: packRules(orgMemberPermissions.rules),
|
||||
permissions: packRules(orgMemberPermissions),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
@@ -84,7 +84,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
||||
name: "No Access",
|
||||
slug: "no-access",
|
||||
description: "No access to any resources in the organization",
|
||||
permissions: packRules(orgNoAccessPermissions.rules),
|
||||
permissions: packRules(orgNoAccessPermissions),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
@@ -151,7 +151,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
||||
name: "Admin",
|
||||
slug: "admin",
|
||||
description: "Complete administration access over the organization",
|
||||
permissions: packRules(orgAdminPermissions.rules),
|
||||
permissions: packRules(orgAdminPermissions),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
@@ -161,7 +161,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
||||
name: "Member",
|
||||
slug: "member",
|
||||
description: "Non-administrative role in an organization",
|
||||
permissions: packRules(orgMemberPermissions.rules),
|
||||
permissions: packRules(orgMemberPermissions),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
@@ -171,7 +171,7 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRol
|
||||
name: "No Access",
|
||||
slug: "no-access",
|
||||
description: "No access to any resources in the organization",
|
||||
permissions: packRules(orgNoAccessPermissions.rules),
|
||||
permissions: packRules(orgNoAccessPermissions),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
} from "@app/db/schemas";
|
||||
import { TProjects } from "@app/db/schemas/projects";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
@@ -90,7 +89,6 @@ type TOrgServiceFactoryDep = {
|
||||
>;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
};
|
||||
@@ -116,7 +114,6 @@ export const orgServiceFactory = ({
|
||||
licenseService,
|
||||
projectRoleDAL,
|
||||
samlConfigDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL
|
||||
}: TOrgServiceFactoryDep) => {
|
||||
@@ -461,12 +458,6 @@ export const orgServiceFactory = ({
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
|
||||
if (org?.authEnforced) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to invite user due to org-level auth enforced for organization"
|
||||
});
|
||||
}
|
||||
|
||||
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
|
||||
if (isEmailInvalid) {
|
||||
throw new BadRequestError({
|
||||
@@ -475,19 +466,6 @@ export const orgServiceFactory = ({
|
||||
});
|
||||
}
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
|
||||
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
|
||||
throw new BadRequestError({
|
||||
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||
});
|
||||
}
|
||||
|
||||
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
|
||||
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
|
||||
throw new BadRequestError({
|
||||
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||
});
|
||||
}
|
||||
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
|
||||
if (isCustomOrgRole) {
|
||||
if (!plan?.rbac)
|
||||
@@ -572,7 +550,7 @@ export const orgServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
const [inviteeMembership] = await orgDAL.findMembership(
|
||||
const [inviteeOrgMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUserId
|
||||
@@ -581,7 +559,27 @@ export const orgServiceFactory = ({
|
||||
);
|
||||
|
||||
// if there exist no org membership we set is as given by the request
|
||||
if (!inviteeMembership) {
|
||||
if (!inviteeOrgMembership) {
|
||||
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
|
||||
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
|
||||
throw new BadRequestError({
|
||||
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||
});
|
||||
}
|
||||
|
||||
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
|
||||
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
|
||||
throw new BadRequestError({
|
||||
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||
});
|
||||
}
|
||||
|
||||
if (org?.authEnforced) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to invite user due to org-level auth enforced for organization"
|
||||
});
|
||||
}
|
||||
|
||||
// as its used by project invite also
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
let roleId;
|
||||
@@ -617,7 +615,6 @@ export const orgServiceFactory = ({
|
||||
}
|
||||
|
||||
const userIds = users.map(({ id }) => id);
|
||||
const usernames = users.map((el) => el.username);
|
||||
const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx);
|
||||
// we don't need to spam with email. Thus org invitation doesn't need project invitation again
|
||||
const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId));
|
||||
@@ -644,12 +641,10 @@ export const orgServiceFactory = ({
|
||||
{ tx }
|
||||
);
|
||||
const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId);
|
||||
const userIdsToExcludeAsPartOfGroup = new Set(
|
||||
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernames, projectId, tx)
|
||||
);
|
||||
const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter(
|
||||
(user) => !existingMembersGroupByUserId?.[user.userId] && !userIdsToExcludeAsPartOfGroup.has(user.userId)
|
||||
(user) => !existingMembersGroupByUserId?.[user.userId]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!userWithEncryptionKeyInvitedToProject.length) continue;
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ export const getBotKeyFnFactory = (
|
||||
) => {
|
||||
const getBotKeyFn = async (projectId: string) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found during bot lookup." });
|
||||
if (!project)
|
||||
throw new BadRequestError({
|
||||
message: "Project not found during bot lookup. Are you sure you are using the correct project ID?"
|
||||
});
|
||||
|
||||
if (project.version === 3) {
|
||||
return { project, shouldUseSecretV2Bridge: true };
|
||||
|
||||
@@ -90,15 +90,20 @@ export const projectMembershipServiceFactory = ({
|
||||
// projectMembers[0].project
|
||||
if (includeGroupMembers) {
|
||||
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
|
||||
|
||||
const allMembers = [
|
||||
...projectMembers.map((m) => ({ ...m, isGroupMember: false })),
|
||||
...groupMembers.map((m) => ({ ...m, isGroupMember: true }))
|
||||
];
|
||||
|
||||
// Ensure the userId is unique
|
||||
const membersIds = new Set(allMembers.map((entity) => entity.user.id));
|
||||
const uniqueMembers = allMembers.filter((entity) => membersIds.has(entity.user.id));
|
||||
const uniqueMembers: typeof allMembers = [];
|
||||
const addedUserIds = new Set<string>();
|
||||
allMembers.forEach((member) => {
|
||||
if (!addedUserIds.has(member.user.id)) {
|
||||
uniqueMembers.push(member);
|
||||
addedUserIds.add(member.user.id);
|
||||
}
|
||||
});
|
||||
|
||||
return uniqueMembers;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { TableName, TProjectEnvironments, TSecretFolders, TSecretFoldersUpdate }
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { groupBy, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
export const validateFolderName = (folderName: string) => {
|
||||
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
|
||||
@@ -83,7 +85,7 @@ const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: str
|
||||
.from<TSecretFolders & { depth: number; path: string }>("parent");
|
||||
};
|
||||
|
||||
const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environment: string, secretPath: string) => {
|
||||
const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environments: string[], secretPath: string) => {
|
||||
// this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2
|
||||
const formatedPath = secretPath.at(-1) === "/" && secretPath.length > 1 ? secretPath.slice(0, -1) : secretPath;
|
||||
// next goal to sanitize saw the raw sql query is safe
|
||||
@@ -111,7 +113,7 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environment: stri
|
||||
projectId,
|
||||
parentId: null
|
||||
})
|
||||
.where(`${TableName.Environment}.slug`, environment)
|
||||
.whereIn(`${TableName.Environment}.slug`, environments)
|
||||
.select(selectAllTableCols(TableName.SecretFolder))
|
||||
.union(
|
||||
(qb) =>
|
||||
@@ -139,14 +141,14 @@ const sqlFindFolderByPathQuery = (db: Knex, projectId: string, environment: stri
|
||||
.from<TSecretFolders & { depth: number; path: string }>("parent")
|
||||
.leftJoin<TProjectEnvironments>(TableName.Environment, `${TableName.Environment}.id`, "parent.envId")
|
||||
.select<
|
||||
TSecretFolders & {
|
||||
(TSecretFolders & {
|
||||
depth: number;
|
||||
path: string;
|
||||
envId: string;
|
||||
envSlug: string;
|
||||
envName: string;
|
||||
projectId: string;
|
||||
}
|
||||
})[]
|
||||
>(
|
||||
selectAllTableCols("parent" as TableName.SecretFolder),
|
||||
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||
@@ -214,7 +216,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
const folder = await sqlFindFolderByPathQuery(
|
||||
tx || db.replicaNode(),
|
||||
projectId,
|
||||
environment,
|
||||
[environment],
|
||||
removeTrailingSlash(path)
|
||||
)
|
||||
.orderBy("depth", "desc")
|
||||
@@ -230,6 +232,35 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
// finds folders by path for multiple envs
|
||||
const findBySecretPathMultiEnv = async (projectId: string, environments: string[], path: string, tx?: Knex) => {
|
||||
try {
|
||||
const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1;
|
||||
|
||||
const folders = await sqlFindFolderByPathQuery(
|
||||
tx || db.replicaNode(),
|
||||
projectId,
|
||||
environments,
|
||||
removeTrailingSlash(path)
|
||||
)
|
||||
.orderBy("depth", "desc")
|
||||
.where("depth", pathDepth);
|
||||
|
||||
const firstFolder = folders[0];
|
||||
|
||||
if (firstFolder && firstFolder.path !== removeTrailingSlash(path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return folders.map((folder) => {
|
||||
const { envId: id, envName: name, envSlug: slug, ...el } = folder;
|
||||
return { ...el, envId: id, environment: { id, name, slug } };
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find folders by secret path multi env" });
|
||||
}
|
||||
};
|
||||
|
||||
// used in folder creation
|
||||
// even if its the original given /path1/path2
|
||||
// it will stop automatically at /path2
|
||||
@@ -238,7 +269,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
const folder = await sqlFindFolderByPathQuery(
|
||||
tx || db.replicaNode(),
|
||||
projectId,
|
||||
environment,
|
||||
[environment],
|
||||
removeTrailingSlash(path)
|
||||
)
|
||||
.orderBy("depth", "desc")
|
||||
@@ -352,14 +383,77 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
// find project folders for multiple envs
|
||||
const findByMultiEnv = async (
|
||||
{
|
||||
environmentIds,
|
||||
parentIds,
|
||||
search,
|
||||
limit,
|
||||
offset = 0,
|
||||
orderBy = SecretsOrderBy.Name,
|
||||
orderDirection = OrderByDirection.ASC
|
||||
}: {
|
||||
environmentIds: string[];
|
||||
parentIds: string[];
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(TableName.SecretFolder)
|
||||
.whereIn("parentId", parentIds)
|
||||
.whereIn("envId", environmentIds)
|
||||
.where("isReserved", false)
|
||||
.where((bd) => {
|
||||
if (search) {
|
||||
void bd.whereILike(`${TableName.SecretFolder}.name`, `%${search}%`);
|
||||
}
|
||||
})
|
||||
.leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.SecretFolder),
|
||||
db.raw(
|
||||
`DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" ${
|
||||
orderDirection ?? OrderByDirection.ASC
|
||||
}) as rank`
|
||||
),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("environment")
|
||||
)
|
||||
.orderBy(`${TableName.SecretFolder}.${orderBy}`, orderDirection);
|
||||
|
||||
if (limit) {
|
||||
const rankOffset = offset + 1; // ranks start from 1
|
||||
return await (tx || db)
|
||||
.with("w", query)
|
||||
.select("*")
|
||||
.from<Awaited<typeof query>[number]>("w")
|
||||
.where("w.rank", ">=", rankOffset)
|
||||
.andWhere("w.rank", "<", rankOffset + limit);
|
||||
}
|
||||
|
||||
const folders = await query;
|
||||
|
||||
return folders;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find folders multi env" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...secretFolderOrm,
|
||||
update,
|
||||
findBySecretPath,
|
||||
findBySecretPathMultiEnv,
|
||||
findById,
|
||||
findByManySecretPath,
|
||||
findSecretPathByFolderIds,
|
||||
findClosestFolder,
|
||||
findByProjectId
|
||||
findByProjectId,
|
||||
findByMultiEnv
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
@@ -26,7 +27,7 @@ type TSecretFolderServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
folderDAL: TSecretFolderDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
||||
folderVersionDAL: TSecretFolderVersionDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
};
|
||||
@@ -396,7 +397,12 @@ export const secretFolderServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
environment,
|
||||
path: secretPath
|
||||
path: secretPath,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
limit,
|
||||
offset
|
||||
}: TGetFolderDTO) => {
|
||||
// folder list is allowed to be read by anyone
|
||||
// permission to check does user has access
|
||||
@@ -408,11 +414,92 @@ export const secretFolderServiceFactory = ({
|
||||
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!parentFolder) return [];
|
||||
|
||||
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id, isReserved: false });
|
||||
const folders = await folderDAL.find(
|
||||
{
|
||||
envId: env.id,
|
||||
parentId: parentFolder.id,
|
||||
isReserved: false,
|
||||
$search: search ? { name: `%${search}%` } : undefined
|
||||
},
|
||||
{
|
||||
sort: orderBy ? [[orderBy, orderDirection ?? OrderByDirection.ASC]] : undefined,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
);
|
||||
return folders;
|
||||
};
|
||||
|
||||
// get folders for multiple envs
|
||||
const getFoldersMultiEnv = async ({
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
environments,
|
||||
path: secretPath,
|
||||
...params
|
||||
}: Omit<TGetFolderDTO, "environment"> & { environments: string[] }) => {
|
||||
// folder list is allowed to be read by anyone
|
||||
// permission to check does user has access
|
||||
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
||||
|
||||
const envs = await projectEnvDAL.findBySlugs(projectId, environments);
|
||||
|
||||
if (!envs.length)
|
||||
throw new BadRequestError({ message: "Environment(s) not found", name: "get project folder count" });
|
||||
|
||||
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
|
||||
if (!parentFolders.length) return [];
|
||||
|
||||
const folders = await folderDAL.findByMultiEnv({
|
||||
environmentIds: envs.map((env) => env.id),
|
||||
parentIds: parentFolders.map((folder) => folder.id),
|
||||
...params
|
||||
});
|
||||
|
||||
return folders;
|
||||
};
|
||||
|
||||
// get the unique count of folders within a project path
|
||||
const getProjectFolderCount = async ({
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
environments,
|
||||
path: secretPath,
|
||||
search
|
||||
}: Omit<TGetFolderDTO, "environment"> & { environments: string[] }) => {
|
||||
// folder list is allowed to be read by anyone
|
||||
// permission to check does user has access
|
||||
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
||||
|
||||
const envs = await projectEnvDAL.findBySlugs(projectId, environments);
|
||||
|
||||
if (!envs.length)
|
||||
throw new BadRequestError({ message: "Environment(s) not found", name: "get project folder count" });
|
||||
|
||||
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
|
||||
if (!parentFolders.length) return 0;
|
||||
|
||||
const folders = await folderDAL.find(
|
||||
{
|
||||
$in: {
|
||||
envId: envs.map((env) => env.id),
|
||||
parentId: parentFolders.map((folder) => folder.id)
|
||||
},
|
||||
isReserved: false,
|
||||
$search: search ? { name: `%${search}%` } : undefined
|
||||
},
|
||||
{ countDistinct: "name" }
|
||||
);
|
||||
|
||||
return Number(folders[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
const getFolderById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetFolderByIdDTO) => {
|
||||
const folder = await folderDAL.findById(id);
|
||||
if (!folder) throw new NotFoundError({ message: "folder not found" });
|
||||
@@ -429,6 +516,8 @@ export const secretFolderServiceFactory = ({
|
||||
updateManyFolders,
|
||||
deleteFolder,
|
||||
getFolders,
|
||||
getFolderById
|
||||
getFolderById,
|
||||
getProjectFolderCount,
|
||||
getFoldersMultiEnv
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
export enum ReservedFolders {
|
||||
SecretReplication = "__reserve_replication_"
|
||||
@@ -36,6 +37,11 @@ export type TDeleteFolderDTO = {
|
||||
export type TGetFolderDTO = {
|
||||
environment: string;
|
||||
path: string;
|
||||
search?: string;
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetFolderByIdDTO = {
|
||||
|
||||
@@ -49,10 +49,30 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (filter: Partial<TSecretImports & { projectId: string }>, tx?: Knex) => {
|
||||
const find = async (
|
||||
{
|
||||
search,
|
||||
limit,
|
||||
offset,
|
||||
...filter
|
||||
}: Partial<
|
||||
TSecretImports & {
|
||||
projectId: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.SecretImport)
|
||||
const query = (tx || db.replicaNode())(TableName.SecretImport)
|
||||
.where(filter)
|
||||
.where((bd) => {
|
||||
if (search) {
|
||||
void bd.whereILike("importPath", `%${search}%`);
|
||||
}
|
||||
})
|
||||
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
|
||||
.select(
|
||||
db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports,
|
||||
@@ -61,6 +81,13 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.Environment).as("envId")
|
||||
)
|
||||
.orderBy("position", "asc");
|
||||
|
||||
if (limit) {
|
||||
void query.limit(limit).offset(offset ?? 0);
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
|
||||
return docs.map(({ envId, slug, name, ...el }) => ({
|
||||
...el,
|
||||
importEnv: { id: envId, slug, name }
|
||||
@@ -70,6 +97,28 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getProjectImportCount = async (
|
||||
{ search, ...filter }: Partial<TSecretImports & { projectId: string; search?: string }>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.SecretImport)
|
||||
.where(filter)
|
||||
.where("isReplication", false)
|
||||
.where((bd) => {
|
||||
if (search) {
|
||||
void bd.whereILike("importPath", `%${search}%`);
|
||||
}
|
||||
})
|
||||
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
|
||||
.count();
|
||||
|
||||
return Number(docs[0]?.count ?? 0);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "get secret imports count" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByFolderIds = async (folderIds: string[], tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.SecretImport)
|
||||
@@ -97,6 +146,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
find,
|
||||
findByFolderIds,
|
||||
findLastImportPosition,
|
||||
updateAllPosition
|
||||
updateAllPosition,
|
||||
getProjectImportCount
|
||||
};
|
||||
};
|
||||
|
||||
@@ -220,7 +220,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
|
||||
|
||||
const processedImports = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
|
||||
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`][0];
|
||||
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`]?.[0];
|
||||
const folderDeeperImportSecrets =
|
||||
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];
|
||||
const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
@@ -394,6 +394,36 @@ export const secretImportServiceFactory = ({
|
||||
return { message: "replication started" };
|
||||
};
|
||||
|
||||
const getProjectImportCount = async ({
|
||||
path: secretPath,
|
||||
environment,
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
search
|
||||
}: TGetSecretImportsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) throw new NotFoundError({ message: "Folder not found", name: "Get imports" });
|
||||
|
||||
const count = await secretImportDAL.getProjectImportCount({ folderId: folder.id, search });
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
const getImports = async ({
|
||||
path: secretPath,
|
||||
environment,
|
||||
@@ -401,7 +431,10 @@ export const secretImportServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
search,
|
||||
limit,
|
||||
offset
|
||||
}: TGetSecretImportsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -418,7 +451,7 @@ export const secretImportServiceFactory = ({
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Get imports" });
|
||||
|
||||
const secImports = await secretImportDAL.find({ folderId: folder.id });
|
||||
const secImports = await secretImportDAL.find({ folderId: folder.id, search, limit, offset });
|
||||
return secImports;
|
||||
};
|
||||
|
||||
@@ -512,7 +545,11 @@ export const secretImportServiceFactory = ({
|
||||
return importedSecrets;
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
if (!botKey)
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
||||
return importedSecrets.map((el) => ({
|
||||
@@ -531,6 +568,7 @@ export const secretImportServiceFactory = ({
|
||||
getSecretsFromImports,
|
||||
getRawSecretsFromImports,
|
||||
resyncSecretImportReplication,
|
||||
getProjectImportCount,
|
||||
fnSecretsFromImports
|
||||
};
|
||||
};
|
||||
|
||||
@@ -32,6 +32,9 @@ export type TDeleteSecretImportDTO = {
|
||||
export type TGetSecretImportsDTO = {
|
||||
environment: string;
|
||||
path: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetSecretsFromImportDTO = {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { TDbClient } from "@app/db";
|
||||
import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFactory>;
|
||||
|
||||
@@ -181,7 +183,16 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
|
||||
// get unique secret count by folder IDs
|
||||
const countByFolderIds = async (
|
||||
folderIds: string[],
|
||||
userId?: string,
|
||||
tx?: Knex,
|
||||
filters?: {
|
||||
search?: string;
|
||||
tagSlugs?: string[];
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
||||
if (userId && !uuidValidate(userId)) {
|
||||
@@ -189,8 +200,70 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
userId = undefined;
|
||||
}
|
||||
|
||||
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
|
||||
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
||||
.whereIn("folderId", folderIds)
|
||||
.where((bd) => {
|
||||
if (filters?.search) {
|
||||
void bd.whereILike("key", `%${filters?.search}%`);
|
||||
}
|
||||
})
|
||||
.where((bd) => {
|
||||
void bd.whereNull("userId").orWhere({ userId: userId || null });
|
||||
})
|
||||
.countDistinct("key");
|
||||
|
||||
// only need to join tags if filtering by tag slugs
|
||||
const slugs = filters?.tagSlugs?.filter(Boolean);
|
||||
if (slugs && slugs.length > 0) {
|
||||
void query
|
||||
.leftJoin(
|
||||
TableName.SecretV2JnTag,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretTag,
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.whereIn("slug", slugs);
|
||||
}
|
||||
|
||||
const secrets = await query;
|
||||
|
||||
return Number(secrets[0]?.count ?? 0);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "get folder secret count" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByFolderIds = async (
|
||||
folderIds: string[],
|
||||
userId?: string,
|
||||
tx?: Knex,
|
||||
filters?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
tagSlugs?: string[];
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
|
||||
if (userId && !uuidValidate(userId)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
userId = undefined;
|
||||
}
|
||||
|
||||
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
||||
.whereIn("folderId", folderIds)
|
||||
.where((bd) => {
|
||||
if (filters?.search) {
|
||||
void bd.whereILike("key", `%${filters?.search}%`);
|
||||
}
|
||||
})
|
||||
.where((bd) => {
|
||||
void bd.whereNull("userId").orWhere({ userId: userId || null });
|
||||
})
|
||||
@@ -204,11 +277,37 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(
|
||||
selectAllTableCols(TableName.SecretV2),
|
||||
db.raw(`DENSE_RANK() OVER (ORDER BY "key" ${filters?.orderDirection ?? OrderByDirection.ASC}) as rank`)
|
||||
)
|
||||
.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"))
|
||||
.orderBy("id", "asc");
|
||||
.where((bd) => {
|
||||
const slugs = filters?.tagSlugs?.filter(Boolean);
|
||||
if (slugs && slugs.length > 0) {
|
||||
void bd.whereIn("slug", slugs);
|
||||
}
|
||||
})
|
||||
.orderBy(
|
||||
filters?.orderBy === SecretsOrderBy.Name ? "key" : "id",
|
||||
filters?.orderDirection ?? OrderByDirection.ASC
|
||||
);
|
||||
|
||||
let secs: Awaited<typeof query>;
|
||||
|
||||
if (filters?.limit) {
|
||||
const rankOffset = (filters?.offset ?? 0) + 1; // ranks start at 1
|
||||
secs = await (tx || db)
|
||||
.with("w", query)
|
||||
.select("*")
|
||||
.from<Awaited<typeof query>[number]>("w")
|
||||
.where("w.rank", ">=", rankOffset)
|
||||
.andWhere("w.rank", "<", rankOffset + filters.limit);
|
||||
} else {
|
||||
secs = await query;
|
||||
}
|
||||
|
||||
const data = sqlNestRelationships({
|
||||
data: secs,
|
||||
@@ -384,6 +483,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
findBySecretKeys,
|
||||
upsertSecretReferences,
|
||||
findReferencedSecretReferences,
|
||||
findAllProjectSecretValues
|
||||
findAllProjectSecretValues,
|
||||
countByFolderIds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
|
||||
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" | "findBySecretPathMultiEnv"
|
||||
>;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
|
||||
@@ -431,6 +431,165 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
// get unique secrets count for multiple envs
|
||||
const getSecretsCountMultiEnv = async ({
|
||||
actorId,
|
||||
path,
|
||||
|
||||
projectId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
environments,
|
||||
...params
|
||||
}: Pick<TGetSecretsDTO, "actorId" | "actor" | "path" | "projectId" | "actorOrgId" | "actorAuthMethod" | "search"> & {
|
||||
environments: string[];
|
||||
}) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
// verify user has access to all environments
|
||||
environments.forEach((environment) =>
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
)
|
||||
);
|
||||
|
||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
|
||||
if (!folders.length) return 0;
|
||||
|
||||
const count = await secretDAL.countByFolderIds(
|
||||
folders.map((folder) => folder.id),
|
||||
actorId,
|
||||
undefined,
|
||||
params
|
||||
);
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
// get secret count for individual env
|
||||
const getSecretsCount = async ({
|
||||
actorId,
|
||||
path,
|
||||
environment,
|
||||
projectId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
...params
|
||||
}: Pick<
|
||||
TGetSecretsDTO,
|
||||
| "actorId"
|
||||
| "actor"
|
||||
| "path"
|
||||
| "projectId"
|
||||
| "actorOrgId"
|
||||
| "actorAuthMethod"
|
||||
| "tagSlugs"
|
||||
| "environment"
|
||||
| "search"
|
||||
>) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) return 0;
|
||||
|
||||
const count = await secretDAL.countByFolderIds([folder.id], actorId, undefined, params);
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
// get secrets for multiple envs
|
||||
const getSecretsMultiEnv = async ({
|
||||
actorId,
|
||||
path,
|
||||
environments,
|
||||
projectId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
...params
|
||||
}: Pick<TGetSecretsDTO, "actorId" | "actor" | "path" | "projectId" | "actorOrgId" | "actorAuthMethod" | "search"> & {
|
||||
environments: string[];
|
||||
}) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
let paths: { folderId: string; path: string; environment: string }[] = [];
|
||||
|
||||
// verify user has access to all environments
|
||||
environments.forEach((environment) =>
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
)
|
||||
);
|
||||
|
||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, path);
|
||||
|
||||
if (!folders.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
paths = folders.map((folder) => ({ folderId: folder.id, path, environment: folder.environment.slug }));
|
||||
|
||||
const groupedPaths = groupBy(paths, (p) => p.folderId);
|
||||
|
||||
const secrets = await secretDAL.findByFolderIds(
|
||||
paths.map((p) => p.folderId),
|
||||
actorId,
|
||||
undefined,
|
||||
params
|
||||
);
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const decryptedSecrets = secrets.map((secret) =>
|
||||
reshapeBridgeSecret(
|
||||
projectId,
|
||||
groupedPaths[secret.folderId][0].environment,
|
||||
groupedPaths[secret.folderId][0].path,
|
||||
{
|
||||
...secret,
|
||||
value: secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: "",
|
||||
comment: secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: ""
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return decryptedSecrets;
|
||||
};
|
||||
|
||||
const getSecrets = async ({
|
||||
actorId,
|
||||
path,
|
||||
@@ -441,8 +600,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
includeImports,
|
||||
recursive,
|
||||
tagSlugs = [],
|
||||
expandSecretReferences: shouldExpandSecretReferences
|
||||
expandSecretReferences: shouldExpandSecretReferences,
|
||||
...params
|
||||
}: TGetSecretsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -490,7 +649,9 @@ export const secretV2BridgeServiceFactory = ({
|
||||
|
||||
const secrets = await secretDAL.findByFolderIds(
|
||||
paths.map((p) => p.folderId),
|
||||
actorId
|
||||
actorId,
|
||||
undefined,
|
||||
params
|
||||
);
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
@@ -509,9 +670,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
: ""
|
||||
})
|
||||
);
|
||||
const filteredSecrets = tagSlugs.length
|
||||
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
|
||||
: decryptedSecrets;
|
||||
|
||||
const expandSecretReferences = expandSecretReferencesFactory({
|
||||
projectId,
|
||||
folderDAL,
|
||||
@@ -520,7 +679,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
|
||||
if (shouldExpandSecretReferences) {
|
||||
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
|
||||
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
|
||||
await Promise.allSettled(
|
||||
Object.keys(secretsGroupByPath).map((groupedPath) =>
|
||||
Promise.allSettled(
|
||||
@@ -541,7 +700,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
|
||||
if (!includeImports) {
|
||||
return {
|
||||
secrets: filteredSecrets
|
||||
secrets: decryptedSecrets
|
||||
};
|
||||
}
|
||||
|
||||
@@ -569,7 +728,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
secrets: filteredSecrets,
|
||||
secrets: decryptedSecrets,
|
||||
imports: importedSecrets
|
||||
};
|
||||
};
|
||||
@@ -1416,6 +1575,9 @@ export const secretV2BridgeServiceFactory = ({
|
||||
getSecrets,
|
||||
getSecretVersions,
|
||||
backfillSecretReferences,
|
||||
moveSecrets
|
||||
moveSecrets,
|
||||
getSecretsCount,
|
||||
getSecretsCountMultiEnv,
|
||||
getSecretsMultiEnv
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
|
||||
@@ -21,6 +22,11 @@ export type TGetSecretsDTO = {
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
tagSlugs?: string[];
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretDTO = {
|
||||
|
||||
@@ -832,7 +832,11 @@ export const createManySecretsRawFnFactory = ({
|
||||
secretDAL
|
||||
});
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
if (!botKey)
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const inputSecrets = secrets.map((secret) => {
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
|
||||
@@ -993,7 +997,11 @@ export const updateManySecretsRawFnFactory = ({
|
||||
return updatedSecrets;
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
if (!botKey)
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
|
||||
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas";
|
||||
import {
|
||||
ProjectMembershipRole,
|
||||
ProjectUpgradeStatus,
|
||||
ProjectVersion,
|
||||
TSecretSnapshotSecretsV2,
|
||||
TSecretVersionsV2
|
||||
} from "@app/db/schemas";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { Actor, EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
||||
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||
@@ -21,6 +29,7 @@ import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version
|
||||
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
||||
@@ -40,13 +49,16 @@ import { expandSecretReferencesFactory, getAllNestedSecretReferences } from "../
|
||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
||||
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
||||
import { TSecretDALFactory } from "./secret-dal";
|
||||
import { interpolateSecrets } from "./secret-fns";
|
||||
import {
|
||||
TCreateSecretReminderDTO,
|
||||
TFailedIntegrationSyncEmailsPayload,
|
||||
THandleReminderDTO,
|
||||
TIntegrationSyncPayload,
|
||||
TRemoveSecretReminderDTO,
|
||||
TSyncSecretsDTO
|
||||
} from "./secret-types";
|
||||
@@ -71,6 +83,7 @@ type TSecretQueueFactoryDep = {
|
||||
secretVersionDAL: TSecretVersionDALFactory;
|
||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||
secretTagDAL: TSecretTagDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
secretVersionTagDAL: TSecretVersionTagDALFactory;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
|
||||
@@ -81,6 +94,7 @@ type TSecretQueueFactoryDep = {
|
||||
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
|
||||
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export type TGetSecrets = {
|
||||
@@ -106,6 +120,7 @@ export const secretQueueFactory = ({
|
||||
secretDAL,
|
||||
secretImportDAL,
|
||||
folderDAL,
|
||||
userDAL,
|
||||
webhookDAL,
|
||||
projectEnvDAL,
|
||||
orgDAL,
|
||||
@@ -125,7 +140,8 @@ export const secretQueueFactory = ({
|
||||
snapshotDAL,
|
||||
snapshotSecretV2BridgeDAL,
|
||||
secretApprovalRequestDAL,
|
||||
keyStore
|
||||
keyStore,
|
||||
auditLogService
|
||||
}: TSecretQueueFactoryDep) => {
|
||||
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -430,7 +446,9 @@ export const secretQueueFactory = ({
|
||||
return content;
|
||||
};
|
||||
|
||||
const syncIntegrations = async (dto: TGetSecrets & { deDupeQueue?: Record<string, boolean> }) => {
|
||||
const syncIntegrations = async (
|
||||
dto: TGetSecrets & { isManual?: boolean; actorId?: string; deDupeQueue?: Record<string, boolean> }
|
||||
) => {
|
||||
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
|
||||
attempts: 3,
|
||||
delay: 1000,
|
||||
@@ -499,6 +517,19 @@ export const secretQueueFactory = ({
|
||||
);
|
||||
};
|
||||
|
||||
const sendFailedIntegrationSyncEmails = async (payload: TFailedIntegrationSyncEmailsPayload) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.isSmtpConfigured) return;
|
||||
|
||||
await queueService.queue(QueueName.IntegrationSync, QueueJobs.SendFailedIntegrationSyncEmails, payload, {
|
||||
jobId: `send-failed-integration-sync-emails-${payload.projectId}-${payload.secretPath}-${payload.environmentSlug}`,
|
||||
delay: 1_000 * 60, // 1 minute
|
||||
|
||||
removeOnFail: true,
|
||||
removeOnComplete: true
|
||||
});
|
||||
};
|
||||
|
||||
queueService.start(QueueName.SecretSync, async (job) => {
|
||||
const {
|
||||
_deDupeQueue: deDupeQueue,
|
||||
@@ -528,7 +559,7 @@ export const secretQueueFactory = ({
|
||||
}
|
||||
}
|
||||
);
|
||||
await syncIntegrations({ secretPath, projectId, environment, deDupeQueue });
|
||||
await syncIntegrations({ secretPath, projectId, environment, deDupeQueue, isManual: false });
|
||||
if (!excludeReplication) {
|
||||
await replicateSecrets({
|
||||
_deDupeReplicationQueue: deDupeReplicationQueue,
|
||||
@@ -544,274 +575,396 @@ export const secretQueueFactory = ({
|
||||
});
|
||||
|
||||
queueService.start(QueueName.IntegrationSync, async (job) => {
|
||||
const { environment, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data;
|
||||
if (depth > MAX_SYNC_SECRET_DEPTH) return;
|
||||
if (job.name === QueueJobs.SendFailedIntegrationSyncEmails) {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) {
|
||||
throw new Error("Secret path not found");
|
||||
const jobPayload = job.data as TFailedIntegrationSyncEmailsPayload;
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(jobPayload.projectId);
|
||||
const project = await projectDAL.findById(jobPayload.projectId);
|
||||
|
||||
// Only send emails to admins, and if its a manual trigger, only send it to the person who triggered it (if actor is admin as well)
|
||||
const filteredProjectMembers = projectMembers
|
||||
.filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin))
|
||||
.filter((member) =>
|
||||
jobPayload.manuallyTriggeredByUserId ? member.userId === jobPayload.manuallyTriggeredByUserId : true
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: filteredProjectMembers.map((member) => member.user.email!),
|
||||
template: SmtpTemplates.IntegrationSyncFailed,
|
||||
subjectLine: `Integration Sync Failed`,
|
||||
substitutions: {
|
||||
syncMessage: jobPayload.count === 1 ? jobPayload.syncMessage : undefined, // We are only displaying the sync message if its a singular integration, so we can just grab the first one in the array.
|
||||
secretPath: jobPayload.secretPath,
|
||||
environment: jobPayload.environmentName,
|
||||
count: jobPayload.count,
|
||||
projectName: project.name,
|
||||
integrationUrl: `${appCfg.SITE_URL}/integrations/${project.id}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// find all imports made with the given environment and secret path
|
||||
const linkSourceDto = {
|
||||
projectId,
|
||||
importEnv: folder.environment.id,
|
||||
importPath: secretPath,
|
||||
isReplication: false
|
||||
};
|
||||
const imports = await secretImportDAL.find(linkSourceDto);
|
||||
|
||||
if (imports.length) {
|
||||
// keep calling sync secret for all the imports made
|
||||
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
|
||||
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
|
||||
const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
|
||||
logger.info(
|
||||
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
await Promise.all(
|
||||
imports
|
||||
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string))
|
||||
// filter out already synced ones
|
||||
.filter(
|
||||
({ folderId }) =>
|
||||
!deDupeQueue[
|
||||
uniqueSecretQueueKey(
|
||||
foldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||
foldersGroupedById[folderId][0]?.path as string
|
||||
)
|
||||
]
|
||||
)
|
||||
.map(({ folderId }) =>
|
||||
syncSecrets({
|
||||
projectId,
|
||||
secretPath: foldersGroupedById[folderId][0]?.path as string,
|
||||
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||
_deDupeQueue: deDupeQueue,
|
||||
_depth: depth + 1,
|
||||
excludeReplication: true
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
let referencedFolderIds;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const secretReferences = await secretV2BridgeDAL.findReferencedSecretReferences(
|
||||
if (job.name === QueueJobs.IntegrationSync) {
|
||||
const {
|
||||
environment,
|
||||
actorId,
|
||||
isManual,
|
||||
projectId,
|
||||
folder.environment.slug,
|
||||
secretPath
|
||||
);
|
||||
referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
|
||||
} else {
|
||||
const secretReferences = await secretDAL.findReferencedSecretReferences(
|
||||
projectId,
|
||||
folder.environment.slug,
|
||||
secretPath
|
||||
);
|
||||
referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
|
||||
}
|
||||
if (referencedFolderIds.length) {
|
||||
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
|
||||
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
|
||||
logger.info(
|
||||
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
await Promise.all(
|
||||
referencedFolderIds
|
||||
.filter((folderId) => Boolean(referencedFoldersGroupedById[folderId][0]?.path))
|
||||
// filter out already synced ones
|
||||
.filter(
|
||||
(folderId) =>
|
||||
!deDupeQueue[
|
||||
uniqueSecretQueueKey(
|
||||
referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||
referencedFoldersGroupedById[folderId][0]?.path as string
|
||||
)
|
||||
]
|
||||
)
|
||||
.map((folderId) =>
|
||||
syncSecrets({
|
||||
projectId,
|
||||
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
|
||||
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||
_deDupeQueue: deDupeQueue,
|
||||
_depth: depth + 1,
|
||||
excludeReplication: true
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
secretPath,
|
||||
depth = 1,
|
||||
deDupeQueue = {}
|
||||
} = job.data as TIntegrationSyncPayload;
|
||||
if (depth > MAX_SYNC_SECRET_DEPTH) return;
|
||||
|
||||
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
|
||||
const toBeSyncedIntegrations = integrations.filter(
|
||||
// note: sync only the integrations sourced from secretPath
|
||||
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
||||
);
|
||||
|
||||
if (!integrations.length) return;
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
|
||||
);
|
||||
|
||||
const lock = await keyStore.acquireLock(
|
||||
[KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
|
||||
10000,
|
||||
{
|
||||
retryCount: 3,
|
||||
retryDelay: 2000
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) {
|
||||
throw new Error("Secret path not found");
|
||||
}
|
||||
);
|
||||
const lockAcquiredTime = new Date();
|
||||
|
||||
const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
|
||||
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
|
||||
);
|
||||
// find all imports made with the given environment and secret path
|
||||
const linkSourceDto = {
|
||||
projectId,
|
||||
importEnv: folder.environment.id,
|
||||
importPath: secretPath,
|
||||
isReplication: false
|
||||
};
|
||||
const imports = await secretImportDAL.find(linkSourceDto);
|
||||
|
||||
// check whether the integration should wait or not
|
||||
if (lastRunSyncIntegrationTimestamp) {
|
||||
const INTEGRATION_INTERVAL = 2000;
|
||||
const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
|
||||
if (isStaleSyncIntegration) {
|
||||
if (imports.length) {
|
||||
// keep calling sync secret for all the imports made
|
||||
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
|
||||
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
|
||||
const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${job.data.depth}]`
|
||||
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
await Promise.all(
|
||||
imports
|
||||
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string))
|
||||
// filter out already synced ones
|
||||
.filter(
|
||||
({ folderId }) =>
|
||||
!deDupeQueue[
|
||||
uniqueSecretQueueKey(
|
||||
foldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||
foldersGroupedById[folderId][0]?.path as string
|
||||
)
|
||||
]
|
||||
)
|
||||
.map(({ folderId }) =>
|
||||
syncSecrets({
|
||||
projectId,
|
||||
secretPath: foldersGroupedById[folderId][0]?.path as string,
|
||||
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||
_deDupeQueue: deDupeQueue,
|
||||
_depth: depth + 1,
|
||||
excludeReplication: true
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
let referencedFolderIds;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const secretReferences = await secretV2BridgeDAL.findReferencedSecretReferences(
|
||||
projectId,
|
||||
folder.environment.slug,
|
||||
secretPath
|
||||
);
|
||||
referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
|
||||
} else {
|
||||
const secretReferences = await secretDAL.findReferencedSecretReferences(
|
||||
projectId,
|
||||
folder.environment.slug,
|
||||
secretPath
|
||||
);
|
||||
referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
|
||||
}
|
||||
if (referencedFolderIds.length) {
|
||||
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
|
||||
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
|
||||
logger.info(
|
||||
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
await Promise.all(
|
||||
referencedFolderIds
|
||||
.filter((folderId) => Boolean(referencedFoldersGroupedById[folderId][0]?.path))
|
||||
// filter out already synced ones
|
||||
.filter(
|
||||
(folderId) =>
|
||||
!deDupeQueue[
|
||||
uniqueSecretQueueKey(
|
||||
referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||
referencedFoldersGroupedById[folderId][0]?.path as string
|
||||
)
|
||||
]
|
||||
)
|
||||
.map((folderId) =>
|
||||
syncSecrets({
|
||||
projectId,
|
||||
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
|
||||
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
|
||||
_deDupeQueue: deDupeQueue,
|
||||
_depth: depth + 1,
|
||||
excludeReplication: true
|
||||
})
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
|
||||
lockAcquiredTime.toISOString(),
|
||||
lastRunSyncIntegrationTimestamp
|
||||
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
|
||||
const toBeSyncedIntegrations = integrations.filter(
|
||||
// note: sync only the integrations sourced from secretPath
|
||||
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
|
||||
);
|
||||
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0)
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// akhilmhdh: this try catch is for lock release
|
||||
try {
|
||||
const secrets = shouldUseSecretV2Bridge
|
||||
? await getIntegrationSecretsV2({
|
||||
environment,
|
||||
projectId,
|
||||
folderId: folder.id,
|
||||
depth: 1,
|
||||
secretPath,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
})
|
||||
: await getIntegrationSecrets({
|
||||
environment,
|
||||
projectId,
|
||||
folderId: folder.id,
|
||||
key: botKey as string,
|
||||
depth: 1,
|
||||
secretPath
|
||||
});
|
||||
const integrationsFailedToSync: { integrationId: string; syncMessage?: string }[] = [];
|
||||
|
||||
for (const integration of toBeSyncedIntegrations) {
|
||||
const integrationAuth = {
|
||||
...integration.integrationAuth,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: integration.projectId
|
||||
};
|
||||
if (!integrations.length) return;
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
|
||||
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(
|
||||
integrationAuth,
|
||||
shouldUseSecretV2Bridge,
|
||||
botKey
|
||||
);
|
||||
let awsAssumeRoleArn = null;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
if (integrationAuth.encryptedAwsAssumeIamRoleArn) {
|
||||
awsAssumeRoleArn = secretManagerDecryptor({
|
||||
cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn)
|
||||
}).toString();
|
||||
}
|
||||
} else if (
|
||||
integrationAuth.awsAssumeIamRoleArnTag &&
|
||||
integrationAuth.awsAssumeIamRoleArnIV &&
|
||||
integrationAuth.awsAssumeIamRoleArnCipherText
|
||||
) {
|
||||
awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText,
|
||||
iv: integrationAuth.awsAssumeIamRoleArnIV,
|
||||
tag: integrationAuth.awsAssumeIamRoleArnTag,
|
||||
key: botKey as string
|
||||
});
|
||||
const lock = await keyStore.acquireLock(
|
||||
[KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
|
||||
10000,
|
||||
{
|
||||
retryCount: 3,
|
||||
retryDelay: 2000
|
||||
}
|
||||
);
|
||||
const lockAcquiredTime = new Date();
|
||||
|
||||
const suffixedSecrets: typeof secrets = {};
|
||||
const metadata = integration.metadata as Record<string, string>;
|
||||
if (metadata) {
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
const prefix = metadata?.secretPrefix || "";
|
||||
const suffix = metadata?.secretSuffix || "";
|
||||
const newKey = prefix + key + suffix;
|
||||
suffixedSecrets[newKey] = secrets[key];
|
||||
});
|
||||
}
|
||||
const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
|
||||
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
|
||||
);
|
||||
|
||||
// akhilmhdh: this try catch is for catching integration error and saving it in db
|
||||
try {
|
||||
// akhilmhdh: this needs to changed later to be more easier to use
|
||||
// at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places
|
||||
const response = await syncIntegrationSecrets({
|
||||
createManySecretsRawFn,
|
||||
updateManySecretsRawFn,
|
||||
integrationDAL,
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
|
||||
accessId: accessId as string,
|
||||
awsAssumeRoleArn,
|
||||
accessToken,
|
||||
projectId,
|
||||
appendices: {
|
||||
prefix: metadata?.secretPrefix || "",
|
||||
suffix: metadata?.secretSuffix || ""
|
||||
}
|
||||
});
|
||||
|
||||
await integrationDAL.updateById(integration.id, {
|
||||
lastSyncJobId: job.id,
|
||||
lastUsed: new Date(),
|
||||
syncMessage: response?.syncMessage ?? "",
|
||||
isSynced: response?.isSynced ?? true
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
err,
|
||||
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]`
|
||||
// check whether the integration should wait or not
|
||||
if (lastRunSyncIntegrationTimestamp) {
|
||||
const INTEGRATION_INTERVAL = 2000;
|
||||
const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
|
||||
if (isStaleSyncIntegration) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
||||
"Unknown error occurred.";
|
||||
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
|
||||
lockAcquiredTime.toISOString(),
|
||||
lastRunSyncIntegrationTimestamp
|
||||
);
|
||||
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0)
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
await integrationDAL.updateById(integration.id, {
|
||||
lastSyncJobId: job.id,
|
||||
lastUsed: new Date(),
|
||||
syncMessage: message,
|
||||
isSynced: false
|
||||
const generateActor = async (): Promise<Actor> => {
|
||||
if (isManual && actorId) {
|
||||
const user = await userDAL.findById(actorId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
userId: user.id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
};
|
||||
};
|
||||
|
||||
// akhilmhdh: this try catch is for lock release
|
||||
try {
|
||||
const secrets = shouldUseSecretV2Bridge
|
||||
? await getIntegrationSecretsV2({
|
||||
environment,
|
||||
projectId,
|
||||
folderId: folder.id,
|
||||
depth: 1,
|
||||
secretPath,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
})
|
||||
: await getIntegrationSecrets({
|
||||
environment,
|
||||
projectId,
|
||||
folderId: folder.id,
|
||||
key: botKey as string,
|
||||
depth: 1,
|
||||
secretPath
|
||||
});
|
||||
|
||||
for (const integration of toBeSyncedIntegrations) {
|
||||
const integrationAuth = {
|
||||
...integration.integrationAuth,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: integration.projectId
|
||||
};
|
||||
|
||||
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(
|
||||
integrationAuth,
|
||||
shouldUseSecretV2Bridge,
|
||||
botKey
|
||||
);
|
||||
let awsAssumeRoleArn = null;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
if (integrationAuth.encryptedAwsAssumeIamRoleArn) {
|
||||
awsAssumeRoleArn = secretManagerDecryptor({
|
||||
cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn)
|
||||
}).toString();
|
||||
}
|
||||
} else if (
|
||||
integrationAuth.awsAssumeIamRoleArnTag &&
|
||||
integrationAuth.awsAssumeIamRoleArnIV &&
|
||||
integrationAuth.awsAssumeIamRoleArnCipherText
|
||||
) {
|
||||
awsAssumeRoleArn = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: integrationAuth.awsAssumeIamRoleArnCipherText,
|
||||
iv: integrationAuth.awsAssumeIamRoleArnIV,
|
||||
tag: integrationAuth.awsAssumeIamRoleArnTag,
|
||||
key: botKey as string
|
||||
});
|
||||
}
|
||||
|
||||
const suffixedSecrets: typeof secrets = {};
|
||||
const metadata = integration.metadata as Record<string, string>;
|
||||
if (metadata) {
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
const prefix = metadata?.secretPrefix || "";
|
||||
const suffix = metadata?.secretSuffix || "";
|
||||
const newKey = prefix + key + suffix;
|
||||
suffixedSecrets[newKey] = secrets[key];
|
||||
});
|
||||
}
|
||||
|
||||
// akhilmhdh: this try catch is for catching integration error and saving it in db
|
||||
try {
|
||||
// akhilmhdh: this needs to changed later to be more easier to use
|
||||
// at present this is not at all extendable like to add a new parameter for just one integration need to modify multiple places
|
||||
const response = await syncIntegrationSecrets({
|
||||
createManySecretsRawFn,
|
||||
updateManySecretsRawFn,
|
||||
integrationDAL,
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
|
||||
accessId: accessId as string,
|
||||
awsAssumeRoleArn,
|
||||
accessToken,
|
||||
projectId,
|
||||
appendices: {
|
||||
prefix: metadata?.secretPrefix || "",
|
||||
suffix: metadata?.secretSuffix || ""
|
||||
}
|
||||
});
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId,
|
||||
actor: await generateActor(),
|
||||
event: {
|
||||
type: EventType.INTEGRATION_SYNCED,
|
||||
metadata: {
|
||||
integrationId: integration.id,
|
||||
isSynced: response?.isSynced ?? true,
|
||||
lastSyncJobId: job?.id ?? "",
|
||||
lastUsed: new Date(),
|
||||
syncMessage: response?.syncMessage ?? ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await integrationDAL.updateById(integration.id, {
|
||||
lastSyncJobId: job.id,
|
||||
lastUsed: new Date(),
|
||||
syncMessage: response?.syncMessage ?? "",
|
||||
isSynced: response?.isSynced ?? true
|
||||
});
|
||||
|
||||
// May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check.
|
||||
if (response?.isSynced === false) {
|
||||
integrationsFailedToSync.push({
|
||||
integrationId: integration.id,
|
||||
syncMessage: response.syncMessage
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
err,
|
||||
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}]`
|
||||
);
|
||||
|
||||
const message =
|
||||
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
||||
"Unknown error occurred.";
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId,
|
||||
actor: await generateActor(),
|
||||
event: {
|
||||
type: EventType.INTEGRATION_SYNCED,
|
||||
metadata: {
|
||||
integrationId: integration.id,
|
||||
isSynced: false,
|
||||
lastSyncJobId: job?.id ?? "",
|
||||
lastUsed: new Date(),
|
||||
syncMessage: message
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await integrationDAL.updateById(integration.id, {
|
||||
lastSyncJobId: job.id,
|
||||
syncMessage: message,
|
||||
isSynced: false
|
||||
});
|
||||
|
||||
integrationsFailedToSync.push({
|
||||
integrationId: integration.id,
|
||||
syncMessage: message
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await lock.release();
|
||||
if (integrationsFailedToSync.length) {
|
||||
await sendFailedIntegrationSyncEmails({
|
||||
count: integrationsFailedToSync.length,
|
||||
environmentName: folder.environment.name,
|
||||
environmentSlug: environment,
|
||||
...(isManual &&
|
||||
actorId && {
|
||||
manuallyTriggeredByUserId: actorId
|
||||
}),
|
||||
projectId,
|
||||
secretPath,
|
||||
syncMessage: integrationsFailedToSync[0].syncMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
|
||||
await keyStore.setItemWithExpiry(
|
||||
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath),
|
||||
KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds,
|
||||
lockAcquiredTime.toISOString()
|
||||
);
|
||||
logger.info("Secret integration sync ended: %s", job.id);
|
||||
await keyStore.setItemWithExpiry(
|
||||
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath),
|
||||
KeyStoreTtls.SetSyncSecretIntegrationLastRunTimestampInSeconds,
|
||||
lockAcquiredTime.toISOString()
|
||||
);
|
||||
logger.info("Secret integration sync ended: %s", job.id);
|
||||
}
|
||||
});
|
||||
|
||||
queueService.start(QueueName.SecretReminder, async ({ data }) => {
|
||||
|
||||
@@ -954,6 +954,120 @@ export const secretServiceFactory = ({
|
||||
return secretsDeleted;
|
||||
};
|
||||
|
||||
const getSecretsCount = async ({
|
||||
projectId,
|
||||
path,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
environment,
|
||||
tagSlugs = [],
|
||||
...v2Params
|
||||
}: Pick<
|
||||
TGetSecretsRawDTO,
|
||||
| "projectId"
|
||||
| "path"
|
||||
| "actor"
|
||||
| "actorId"
|
||||
| "actorOrgId"
|
||||
| "actorAuthMethod"
|
||||
| "environment"
|
||||
| "tagSlugs"
|
||||
| "search"
|
||||
>) => {
|
||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
if (!shouldUseSecretV2Bridge)
|
||||
throw new BadRequestError({
|
||||
message: "Project version does not support pagination",
|
||||
name: "pagination_not_supported"
|
||||
});
|
||||
|
||||
const count = await secretV2BridgeService.getSecretsCount({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
environment,
|
||||
path,
|
||||
actorAuthMethod,
|
||||
tagSlugs,
|
||||
...v2Params
|
||||
});
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
const getSecretsCountMultiEnv = async ({
|
||||
projectId,
|
||||
path,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
environments,
|
||||
...v2Params
|
||||
}: Pick<
|
||||
TGetSecretsRawDTO,
|
||||
"projectId" | "path" | "actor" | "actorId" | "actorOrgId" | "actorAuthMethod" | "search"
|
||||
> & { environments: string[] }) => {
|
||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
if (!shouldUseSecretV2Bridge)
|
||||
throw new BadRequestError({
|
||||
message: "Project version does not support pagination",
|
||||
name: "pagination_not_supported"
|
||||
});
|
||||
|
||||
const count = await secretV2BridgeService.getSecretsCountMultiEnv({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
environments,
|
||||
path,
|
||||
actorAuthMethod,
|
||||
...v2Params
|
||||
});
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
const getSecretsRawMultiEnv = async ({
|
||||
projectId,
|
||||
path,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
environments,
|
||||
...params
|
||||
}: Omit<TGetSecretsRawDTO, "environment" | "includeImports" | "expandSecretReferences" | "recursive" | "tagSlugs"> & {
|
||||
environments: string[];
|
||||
}) => {
|
||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
if (!shouldUseSecretV2Bridge)
|
||||
throw new BadRequestError({
|
||||
message: "Project version does not support pagination",
|
||||
name: "pagination_not_supported"
|
||||
});
|
||||
|
||||
const secrets = await secretV2BridgeService.getSecretsMultiEnv({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
environments,
|
||||
path,
|
||||
actorAuthMethod,
|
||||
...params
|
||||
});
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
const getSecretsRaw = async ({
|
||||
projectId,
|
||||
path,
|
||||
@@ -965,7 +1079,8 @@ export const secretServiceFactory = ({
|
||||
includeImports,
|
||||
expandSecretReferences,
|
||||
recursive,
|
||||
tagSlugs = []
|
||||
tagSlugs = [],
|
||||
...paramsV2
|
||||
}: TGetSecretsRawDTO) => {
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
@@ -980,12 +1095,17 @@ export const secretServiceFactory = ({
|
||||
recursive,
|
||||
actorAuthMethod,
|
||||
includeImports,
|
||||
tagSlugs
|
||||
tagSlugs,
|
||||
...paramsV2
|
||||
});
|
||||
return { secrets, imports };
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
if (!botKey)
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
const { secrets, imports } = await getSecrets({
|
||||
actorId,
|
||||
@@ -1146,7 +1266,10 @@ export const secretServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!botKey)
|
||||
throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
|
||||
|
||||
if (expandSecretReferences) {
|
||||
@@ -1238,7 +1361,11 @@ export const secretServiceFactory = ({
|
||||
return { secret, type: SecretProtectionType.Direct as const };
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
if (!botKey)
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
|
||||
@@ -1376,7 +1503,11 @@ export const secretServiceFactory = ({
|
||||
return { type: SecretProtectionType.Direct as const, secret };
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
if (!botKey)
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
|
||||
@@ -1498,7 +1629,11 @@ export const secretServiceFactory = ({
|
||||
});
|
||||
return { type: SecretProtectionType.Direct as const, secret };
|
||||
}
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
if (!botKey)
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
if (policy) {
|
||||
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
||||
policy,
|
||||
@@ -1598,7 +1733,11 @@ export const secretServiceFactory = ({
|
||||
return { secrets, type: SecretProtectionType.Direct as const };
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
if (!botKey)
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const sanitizedSecrets = inputSecrets.map(
|
||||
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey);
|
||||
@@ -1720,7 +1859,11 @@ export const secretServiceFactory = ({
|
||||
return { type: SecretProtectionType.Direct as const, secrets };
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
if (!botKey)
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
const sanitizedSecrets = inputSecrets.map(
|
||||
({
|
||||
secretComment,
|
||||
@@ -1848,7 +1991,11 @@ export const secretServiceFactory = ({
|
||||
return { type: SecretProtectionType.Direct as const, secrets };
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
if (!botKey)
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
if (policy) {
|
||||
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
|
||||
@@ -2182,7 +2329,10 @@ export const secretServiceFactory = ({
|
||||
}
|
||||
|
||||
if (!botKey)
|
||||
throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
await secretDAL.transaction(async (tx) => {
|
||||
const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx);
|
||||
@@ -2265,7 +2415,10 @@ export const secretServiceFactory = ({
|
||||
|
||||
const { botKey } = await projectBotService.getBotKey(project.id);
|
||||
if (!botKey) {
|
||||
throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
|
||||
throw new BadRequestError({
|
||||
message: "Project bot not found. Please upgrade your project.",
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
}
|
||||
|
||||
const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath);
|
||||
@@ -2656,6 +2809,9 @@ export const secretServiceFactory = ({
|
||||
getSecretVersions,
|
||||
backfillSecretReferences,
|
||||
moveSecrets,
|
||||
startSecretV2Migration
|
||||
startSecretV2Migration,
|
||||
getSecretsCount,
|
||||
getSecretsCountMultiEnv,
|
||||
getSecretsRawMultiEnv
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Knex } from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||
@@ -21,6 +22,29 @@ type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secret
|
||||
|
||||
type TPartialInputSecret = Pick<TSecrets, "type" | "secretReminderNote" | "secretReminderRepeatDays" | "id">;
|
||||
|
||||
export const FailedIntegrationSyncEmailsPayloadSchema = z.object({
|
||||
projectId: z.string(),
|
||||
secretPath: z.string(),
|
||||
environmentName: z.string(),
|
||||
environmentSlug: z.string(),
|
||||
|
||||
count: z.number(),
|
||||
syncMessage: z.string().optional(),
|
||||
manuallyTriggeredByUserId: z.string().optional()
|
||||
});
|
||||
|
||||
export type TFailedIntegrationSyncEmailsPayload = z.infer<typeof FailedIntegrationSyncEmailsPayloadSchema>;
|
||||
|
||||
export type TIntegrationSyncPayload = {
|
||||
isManual?: boolean;
|
||||
actorId?: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
depth?: number;
|
||||
deDupeQueue?: Record<string, boolean>;
|
||||
};
|
||||
|
||||
export type TCreateSecretDTO = {
|
||||
secretName: string;
|
||||
path: string;
|
||||
@@ -81,6 +105,8 @@ export type TGetSecretsDTO = {
|
||||
environment: string;
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretDTO = {
|
||||
@@ -143,6 +169,10 @@ export type TDeleteBulkSecretDTO = {
|
||||
}>;
|
||||
} & TProjectPermission;
|
||||
|
||||
export enum SecretsOrderBy {
|
||||
Name = "name" // "key" for secrets but using name for use across resources
|
||||
}
|
||||
|
||||
export type TGetSecretsRawDTO = {
|
||||
expandSecretReferences?: boolean;
|
||||
path: string;
|
||||
@@ -150,6 +180,11 @@ export type TGetSecretsRawDTO = {
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
tagSlugs?: string[];
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretRawDTO = {
|
||||
|
||||
@@ -33,7 +33,8 @@ export enum SmtpTemplates {
|
||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
||||
PkiExpirationAlert = "pkiExpirationAlert.handlebars"
|
||||
PkiExpirationAlert = "pkiExpirationAlert.handlebars",
|
||||
IntegrationSyncFailed = "integrationSyncFailed.handlebars"
|
||||
}
|
||||
|
||||
export enum SmtpHost {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Integration Sync Failed</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
|
||||
<div>
|
||||
<p>{{count}} integration(s) failed to sync.</p>
|
||||
<a href="{{integrationUrl}}">
|
||||
View your project integrations.
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div>
|
||||
<p><strong>Project</strong>: {{projectName}}</p>
|
||||
<p><strong>Environment</strong>: {{environment}}</p>
|
||||
<p><strong>Secret Path</strong>: {{secretPath}}</p>
|
||||
</div>
|
||||
|
||||
{{#if syncMessage}}
|
||||
<p><b>Reason: </b>{{syncMessage}}</p>
|
||||
{{/if}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user