mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 07:35:09 +00:00
Merge branch 'main' into feat-admin-signup
This commit is contained in:
@@ -281,8 +281,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
||||
name: "Oauth 2 login"
|
||||
});
|
||||
}
|
||||
|
||||
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] });
|
||||
|
||||
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod], isGhost: false });
|
||||
|
||||
}
|
||||
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||
const isUserCompleted = user.isAccepted;
|
||||
|
||||
@@ -50,7 +50,7 @@ export const authSignupServiceFactory = ({
|
||||
throw new Error("Failed to send verification code for complete account");
|
||||
}
|
||||
if (!user) {
|
||||
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email });
|
||||
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email, isGhost: false });
|
||||
}
|
||||
if (!user) throw new Error("Failed to create user");
|
||||
|
||||
@@ -212,13 +212,16 @@ export const authSignupServiceFactory = ({
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag
|
||||
encryptedPrivateKeyTag,
|
||||
authorization
|
||||
}: TCompleteAccountInviteDTO) => {
|
||||
const user = await userDAL.findUserByEmail(email);
|
||||
if (!user || (user && user.isAccepted)) {
|
||||
throw new Error("Failed to complete account for complete user");
|
||||
}
|
||||
|
||||
validateSignUpAuthorization(authorization, user.id);
|
||||
|
||||
const [orgMembership] = await orgDAL.findMembership({
|
||||
inviteEmail: email,
|
||||
status: OrgMembershipStatus.Invited
|
||||
|
||||
@@ -34,4 +34,5 @@ export type TCompleteAccountInviteDTO = {
|
||||
verifier: string;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
authorization: string;
|
||||
};
|
||||
|
||||
@@ -17,21 +17,24 @@ export enum AuthTokenType {
|
||||
API_KEY = "apiKey",
|
||||
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||
SERVICE_REFRESH_TOKEN = "serviceRefreshToken",
|
||||
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
|
||||
IDENTITY_ACCESS_TOKEN = "identityAccessToken",
|
||||
SCIM_TOKEN = "scimToken"
|
||||
}
|
||||
|
||||
export enum AuthMode {
|
||||
JWT = "jwt",
|
||||
SERVICE_TOKEN = "serviceToken",
|
||||
API_KEY = "apiKey",
|
||||
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
|
||||
IDENTITY_ACCESS_TOKEN = "identityAccessToken",
|
||||
SCIM_TOKEN = "scimToken"
|
||||
}
|
||||
|
||||
export enum ActorType { // would extend to AWS, Azure, ...
|
||||
USER = "user", // userIdentity
|
||||
SERVICE = "service",
|
||||
IDENTITY = "identity",
|
||||
Machine = "machine"
|
||||
Machine = "machine",
|
||||
SCIM_CLIENT = "scimClient"
|
||||
}
|
||||
|
||||
export type AuthModeJwtTokenPayload = {
|
||||
|
||||
@@ -35,12 +35,12 @@ export const identityAccessTokenServiceFactory = ({
|
||||
}
|
||||
|
||||
// ttl check
|
||||
if (accessTokenTTL > 0) {
|
||||
if (Number(accessTokenTTL) > 0) {
|
||||
const currentDate = new Date();
|
||||
if (accessTokenLastRenewedAt) {
|
||||
// access token has been renewed
|
||||
const accessTokenRenewed = new Date(accessTokenLastRenewedAt);
|
||||
const ttlInMilliseconds = accessTokenTTL * 1000;
|
||||
const ttlInMilliseconds = Number(accessTokenTTL) * 1000;
|
||||
const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds);
|
||||
|
||||
if (currentDate > expirationDate)
|
||||
@@ -50,7 +50,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
} else {
|
||||
// access token has never been renewed
|
||||
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
||||
const ttlInMilliseconds = accessTokenTTL * 1000;
|
||||
const ttlInMilliseconds = Number(accessTokenTTL) * 1000;
|
||||
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
||||
|
||||
if (currentDate > expirationDate)
|
||||
@@ -61,9 +61,9 @@ export const identityAccessTokenServiceFactory = ({
|
||||
}
|
||||
|
||||
// max ttl checks
|
||||
if (accessTokenMaxTTL > 0) {
|
||||
if (Number(accessTokenMaxTTL) > 0) {
|
||||
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
||||
const ttlInMilliseconds = accessTokenMaxTTL * 1000;
|
||||
const ttlInMilliseconds = Number(accessTokenMaxTTL) * 1000;
|
||||
const currentDate = new Date();
|
||||
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
||||
|
||||
@@ -72,7 +72,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
message: "Failed to renew MI access token due to Max TTL expiration"
|
||||
});
|
||||
|
||||
const extendToDate = new Date(currentDate.getTime() + accessTokenTTL);
|
||||
const extendToDate = new Date(currentDate.getTime() + Number(accessTokenTTL));
|
||||
if (extendToDate > expirationDate)
|
||||
throw new UnauthorizedError({
|
||||
message: "Failed to renew MI access token past its Max TTL expiration"
|
||||
|
||||
@@ -69,9 +69,9 @@ export const identityUaServiceFactory = ({
|
||||
if (!validClientSecretInfo) throw new UnauthorizedError();
|
||||
|
||||
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
||||
if (clientSecretTTL > 0) {
|
||||
if (Number(clientSecretTTL) > 0) {
|
||||
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
||||
const ttlInMilliseconds = clientSecretTTL * 1000;
|
||||
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
|
||||
const currentDate = new Date();
|
||||
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
||||
|
||||
@@ -124,7 +124,10 @@ export const identityUaServiceFactory = ({
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
{
|
||||
expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL
|
||||
expiresIn:
|
||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
||||
? undefined
|
||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,35 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TIntegrationAuths, TIntegrationAuthsUpdate } from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIntegrationAuthDALFactory = ReturnType<typeof integrationAuthDALFactory>;
|
||||
|
||||
export const integrationAuthDALFactory = (db: TDbClient) => {
|
||||
const integrationAuthOrm = ormify(db, TableName.IntegrationAuth);
|
||||
return integrationAuthOrm;
|
||||
|
||||
const bulkUpdate = async (
|
||||
data: Array<{ filter: Partial<TIntegrationAuths>; data: TIntegrationAuthsUpdate }>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const integrationAuths = await Promise.all(
|
||||
data.map(async ({ filter, data: updateData }) => {
|
||||
const [doc] = await (tx || db)(TableName.IntegrationAuth).where(filter).update(updateData).returning("*");
|
||||
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
|
||||
return doc;
|
||||
})
|
||||
);
|
||||
return integrationAuths;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...integrationAuthOrm,
|
||||
bulkUpdate
|
||||
};
|
||||
};
|
||||
|
||||
@@ -76,7 +76,8 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
);
|
||||
)
|
||||
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
|
||||
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||
...data,
|
||||
user: { email, firstName, lastName, id: userId, publicKey }
|
||||
@@ -86,6 +87,79 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findOrgMembersByEmail = async (orgId: string, emails: string[]) => {
|
||||
try {
|
||||
const members = await db(TableName.OrgMembership)
|
||||
.where({ orgId })
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin<TUserEncryptionKeys>(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.UserEncryptionKey}.userId`,
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.OrgMembership),
|
||||
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||
db.ref("orgId").withSchema(TableName.OrgMembership),
|
||||
db.ref("role").withSchema(TableName.OrgMembership),
|
||||
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||
db.ref("status").withSchema(TableName.OrgMembership),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
)
|
||||
.where({ isGhost: false })
|
||||
.whereIn("email", emails);
|
||||
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||
...data,
|
||||
user: { email, firstName, lastName, id: userId, publicKey }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all org members" });
|
||||
}
|
||||
};
|
||||
|
||||
const findOrgGhostUser = async (orgId: string) => {
|
||||
try {
|
||||
const member = await db(TableName.OrgMembership)
|
||||
.where({ orgId })
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.OrgMembership),
|
||||
db.ref("orgId").withSchema(TableName.OrgMembership),
|
||||
db.ref("role").withSchema(TableName.OrgMembership),
|
||||
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||
db.ref("status").withSchema(TableName.OrgMembership),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
)
|
||||
.where({ isGhost: true })
|
||||
.first();
|
||||
return member;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const ghostUserExists = async (orgId: string) => {
|
||||
try {
|
||||
const member = await db(TableName.OrgMembership)
|
||||
.where({ orgId })
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
||||
.select(db.ref("id").withSchema(TableName.Users).as("userId"))
|
||||
.where({ isGhost: true })
|
||||
.first();
|
||||
return Boolean(member);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (dto: TOrganizationsInsert, tx?: Knex) => {
|
||||
try {
|
||||
const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
|
||||
@@ -165,7 +239,14 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
// eslint-disable-next-line
|
||||
.where(buildFindFilter(filter))
|
||||
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
||||
.select(selectAllTableCols(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users));
|
||||
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.OrgMembership),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("scimEnabled").withSchema(TableName.Organization)
|
||||
);
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
@@ -184,6 +265,9 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
findAllOrgMembers,
|
||||
findOrgById,
|
||||
findAllOrgsByUserId,
|
||||
ghostUserExists,
|
||||
findOrgMembersByEmail,
|
||||
findOrgGhostUser,
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
|
||||
type TDeleteOrgMembership = {
|
||||
orgMembershipId: string;
|
||||
orgId: string;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
|
||||
};
|
||||
|
||||
export const deleteOrgMembership = async ({
|
||||
orgMembershipId,
|
||||
orgId,
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL
|
||||
}: TDeleteOrgMembership) => {
|
||||
const membership = await orgDAL.transaction(async (tx) => {
|
||||
// delete org membership
|
||||
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
|
||||
|
||||
const projects = await projectDAL.find({ orgId }, { tx });
|
||||
|
||||
// delete associated project memberships
|
||||
await projectMembershipDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
projectId: projects.map((project) => project.id)
|
||||
},
|
||||
userId: orgMembership.userId as string
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return orgMembership;
|
||||
});
|
||||
|
||||
return membership;
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import crypto from "crypto";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
||||
import { TProjects } from "@app/db/schemas/projects";
|
||||
@@ -11,6 +13,7 @@ import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
@@ -28,6 +31,7 @@ import { TOrgRoleDALFactory } from "./org-role-dal";
|
||||
import {
|
||||
TDeleteOrgMembershipDTO,
|
||||
TFindAllWorkspacesDTO,
|
||||
TFindOrgMembersByEmailDTO,
|
||||
TInviteUserToOrgDTO,
|
||||
TUpdateOrgDTO,
|
||||
TUpdateOrgMembershipDTO,
|
||||
@@ -93,6 +97,15 @@ export const orgServiceFactory = ({
|
||||
return members;
|
||||
};
|
||||
|
||||
const findOrgMembersByEmail = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
|
||||
const members = await orgDAL.findOrgMembersByEmail(orgId, emails);
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
const findAllWorkspaces = async ({ actor, actorId, actorOrgId, orgId }: TFindAllWorkspacesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
@@ -118,6 +131,54 @@ export const orgServiceFactory = ({
|
||||
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
|
||||
};
|
||||
|
||||
const addGhostUser = async (orgId: string, tx?: Knex) => {
|
||||
const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
|
||||
const password = crypto.randomBytes(128).toString("hex");
|
||||
|
||||
const user = await userDAL.create(
|
||||
{
|
||||
isGhost: true,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
email,
|
||||
isAccepted: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const encKeys = await generateUserSrpKeys(email, password);
|
||||
|
||||
await userDAL.upsertUserEncryptionKey(
|
||||
user.id,
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey: encKeys.protectedKey,
|
||||
protectedKeyIV: encKeys.protectedKeyIV,
|
||||
protectedKeyTag: encKeys.protectedKeyTag,
|
||||
publicKey: encKeys.publicKey,
|
||||
encryptedPrivateKey: encKeys.encryptedPrivateKey,
|
||||
iv: encKeys.encryptedPrivateKeyIV,
|
||||
tag: encKeys.encryptedPrivateKeyTag,
|
||||
salt: encKeys.salt,
|
||||
verifier: encKeys.verifier
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const createMembershipData = {
|
||||
orgId,
|
||||
userId: user.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
};
|
||||
|
||||
await orgDAL.createMembership(createMembershipData, tx);
|
||||
|
||||
return {
|
||||
user,
|
||||
keys: encKeys
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Update organization details
|
||||
* */
|
||||
@@ -126,16 +187,32 @@ export const orgServiceFactory = ({
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId,
|
||||
data: { name, slug, authEnforced }
|
||||
data: { name, slug, authEnforced, scimEnabled }
|
||||
}: TUpdateOrgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
|
||||
if (authEnforced !== undefined) {
|
||||
if (!plan?.samlSSO)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to enforce/un-enforce SAML SSO due to plan restriction. Upgrade plan to enforce/un-enforce SAML SSO."
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||
}
|
||||
|
||||
if (authEnforced) {
|
||||
if (scimEnabled !== undefined) {
|
||||
if (!plan?.scim)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to enable/disable SCIM provisioning due to plan restriction. Upgrade plan to enable/disable SCIM provisioning."
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
||||
}
|
||||
|
||||
if (authEnforced || scimEnabled) {
|
||||
const samlCfg = await samlConfigDAL.findEnforceableSamlCfg(orgId);
|
||||
if (!samlCfg)
|
||||
throw new BadRequestError({
|
||||
@@ -147,7 +224,8 @@ export const orgServiceFactory = ({
|
||||
const org = await orgDAL.updateById(orgId, {
|
||||
name,
|
||||
slug: slug ? slugify(slug) : undefined,
|
||||
authEnforced
|
||||
authEnforced,
|
||||
scimEnabled
|
||||
});
|
||||
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
|
||||
return org;
|
||||
@@ -321,7 +399,8 @@ export const orgServiceFactory = ({
|
||||
{
|
||||
email: inviteeEmail,
|
||||
isAccepted: false,
|
||||
authMethods: [AuthMethod.EMAIL]
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -470,10 +549,12 @@ export const orgServiceFactory = ({
|
||||
inviteUserToOrganization,
|
||||
verifyUserToOrg,
|
||||
updateOrg,
|
||||
findOrgMembersByEmail,
|
||||
createOrganization,
|
||||
deleteOrganizationById,
|
||||
deleteOrgMembership,
|
||||
findAllWorkspaces,
|
||||
addGhostUser,
|
||||
updateOrgMembership,
|
||||
// incident contacts
|
||||
findIncidentContacts,
|
||||
|
||||
@@ -30,6 +30,13 @@ export type TVerifyUserToOrgDTO = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type TFindOrgMembersByEmailDTO = {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
orgId: string;
|
||||
emails: string[];
|
||||
};
|
||||
|
||||
export type TFindAllWorkspacesDTO = {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
@@ -38,5 +45,5 @@ export type TFindAllWorkspacesDTO = {
|
||||
};
|
||||
|
||||
export type TUpdateOrgDTO = {
|
||||
data: Partial<{ name: string; slug: string; authEnforced: boolean }>;
|
||||
data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>;
|
||||
} & TOrgPermission;
|
||||
|
||||
@@ -27,5 +27,19 @@ export const projectBotDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...projectBotOrm, findOne };
|
||||
const findProjectByBotId = async (botId: string) => {
|
||||
try {
|
||||
const project = await db(TableName.ProjectBot)
|
||||
.where({ [`${TableName.ProjectBot}.id` as "id"]: botId })
|
||||
.join(TableName.Project, `${TableName.ProjectBot}.projectId`, `${TableName.Project}.id`)
|
||||
.select(selectAllTableCols(TableName.Project))
|
||||
.first();
|
||||
|
||||
return project || null;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find project by bot id" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...projectBotOrm, findOne, findProjectByBotId };
|
||||
};
|
||||
|
||||
@@ -1,125 +1,111 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import {
|
||||
decryptAsymmetric,
|
||||
decryptSymmetric,
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
encryptSymmetric,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
generateAsymmetricKeyPair
|
||||
} from "@app/lib/crypto";
|
||||
import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectBotDALFactory } from "./project-bot-dal";
|
||||
import { TSetActiveStateDTO } from "./project-bot-types";
|
||||
import { TFindBotByProjectIdDTO, TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types";
|
||||
|
||||
type TProjectBotServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
projectBotDAL: TProjectBotDALFactory;
|
||||
};
|
||||
|
||||
export type TProjectBotServiceFactory = ReturnType<typeof projectBotServiceFactory>;
|
||||
|
||||
export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: TProjectBotServiceFactoryDep) => {
|
||||
const getBotKey = async (projectId: string) => {
|
||||
const appCfg = getConfig();
|
||||
const encryptionKey = appCfg.ENCRYPTION_KEY;
|
||||
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
|
||||
export const projectBotServiceFactory = ({
|
||||
projectBotDAL,
|
||||
projectDAL,
|
||||
permissionService
|
||||
}: TProjectBotServiceFactoryDep) => {
|
||||
const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const getBotKey = async (projectId: string) => {
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
|
||||
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
|
||||
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
|
||||
throw new BadRequestError({ message: "Encryption key missing" });
|
||||
|
||||
if (rootEncryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.BASE64) {
|
||||
const privateKeyBot = decryptSymmetric({
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey,
|
||||
key: rootEncryptionKey
|
||||
});
|
||||
return decryptAsymmetric({
|
||||
ciphertext: bot.encryptedProjectKey,
|
||||
privateKey: privateKeyBot,
|
||||
nonce: bot.encryptedProjectKeyNonce,
|
||||
publicKey: bot.sender.publicKey
|
||||
});
|
||||
}
|
||||
if (encryptionKey && (bot.keyEncoding as SecretKeyEncoding) === SecretKeyEncoding.UTF8) {
|
||||
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey,
|
||||
key: encryptionKey
|
||||
});
|
||||
return decryptAsymmetric({
|
||||
ciphertext: bot.encryptedProjectKey,
|
||||
privateKey: privateKeyBot,
|
||||
nonce: bot.encryptedProjectKeyNonce,
|
||||
publicKey: bot.sender.publicKey
|
||||
});
|
||||
}
|
||||
const botPrivateKey = getBotPrivateKey({ bot });
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Failed to obtain bot copy of workspace key needed for operation"
|
||||
return decryptAsymmetric({
|
||||
ciphertext: bot.encryptedProjectKey,
|
||||
privateKey: botPrivateKey,
|
||||
nonce: bot.encryptedProjectKeyNonce,
|
||||
publicKey: bot.sender.publicKey
|
||||
});
|
||||
};
|
||||
|
||||
const findBotByProjectId = async ({ actorId, actor, actorOrgId, projectId }: TProjectPermission) => {
|
||||
const findBotByProjectId = async ({
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
actorOrgId,
|
||||
privateKey,
|
||||
botKey,
|
||||
publicKey
|
||||
}: TFindBotByProjectIdDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
const appCfg = getConfig();
|
||||
|
||||
const bot = await projectBotDAL.transaction(async (tx) => {
|
||||
const doc = await projectBotDAL.findOne({ projectId }, tx);
|
||||
if (doc) return doc;
|
||||
|
||||
const { publicKey, privateKey } = generateAsymmetricKeyPair();
|
||||
if (appCfg.ROOT_ENCRYPTION_KEY) {
|
||||
const { iv, tag, ciphertext } = encryptSymmetric(privateKey, appCfg.ROOT_ENCRYPTION_KEY);
|
||||
return projectBotDAL.create(
|
||||
{
|
||||
name: "Infisical Bot",
|
||||
projectId,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: false,
|
||||
publicKey,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.BASE64
|
||||
},
|
||||
tx
|
||||
);
|
||||
const keys = privateKey && publicKey ? { privateKey, publicKey } : generateAsymmetricKeyPair();
|
||||
|
||||
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(keys.privateKey);
|
||||
|
||||
const project = await projectDAL.findById(projectId, tx);
|
||||
|
||||
if (project.version === ProjectVersion.V2) {
|
||||
throw new BadRequestError({ message: "Failed to create bot, project is upgraded." });
|
||||
}
|
||||
if (appCfg.ENCRYPTION_KEY) {
|
||||
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(privateKey, appCfg.ENCRYPTION_KEY);
|
||||
return projectBotDAL.create(
|
||||
{
|
||||
name: "Infisical Bot",
|
||||
projectId,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: false,
|
||||
publicKey,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.UTF8
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
throw new BadRequestError({ message: "Failed to create bot due to missing encryption key" });
|
||||
|
||||
return projectBotDAL.create(
|
||||
{
|
||||
name: "Infisical Bot",
|
||||
projectId,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: false,
|
||||
publicKey: keys.publicKey,
|
||||
algorithm,
|
||||
keyEncoding: encoding,
|
||||
...(botKey && {
|
||||
encryptedProjectKey: botKey.encryptedKey,
|
||||
encryptedProjectKeyNonce: botKey.nonce
|
||||
})
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
return bot;
|
||||
};
|
||||
|
||||
const findProjectByBotId = async (botId: string) => {
|
||||
try {
|
||||
const bot = await projectBotDAL.findProjectByBotId(botId);
|
||||
return bot;
|
||||
} catch (e) {
|
||||
throw new BadRequestError({ message: "Failed to find bot by ID" });
|
||||
}
|
||||
};
|
||||
|
||||
const setBotActiveState = async ({ actor, botId, botKey, actorId, actorOrgId, isActive }: TSetActiveStateDTO) => {
|
||||
const bot = await projectBotDAL.findById(botId);
|
||||
if (!bot) throw new BadRequestError({ message: "Bot not found" });
|
||||
@@ -127,6 +113,16 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, bot.projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||
|
||||
const project = await projectBotDAL.findProjectByBotId(botId);
|
||||
|
||||
if (!project) {
|
||||
throw new BadRequestError({ message: "Failed to find project by bot ID" });
|
||||
}
|
||||
|
||||
if (project.version === ProjectVersion.V2) {
|
||||
throw new BadRequestError({ message: "Failed to set bot active for upgraded project. Bot is already active" });
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
if (!botKey?.nonce || !botKey?.encryptedKey) {
|
||||
throw new BadRequestError({ message: "Failed to set bot active - missing bot key" });
|
||||
@@ -153,6 +149,8 @@ export const projectBotServiceFactory = ({ projectBotDAL, permissionService }: T
|
||||
return {
|
||||
findBotByProjectId,
|
||||
setBotActiveState,
|
||||
getBotPrivateKey,
|
||||
findProjectByBotId,
|
||||
getBotKey
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TProjectBots } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TSetActiveStateDTO = {
|
||||
@@ -8,3 +9,16 @@ export type TSetActiveStateDTO = {
|
||||
};
|
||||
botId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TFindBotByProjectIdDTO = {
|
||||
privateKey?: string;
|
||||
publicKey?: string;
|
||||
botKey?: {
|
||||
nonce: string;
|
||||
encryptedKey: string;
|
||||
};
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetPrivateKeyDTO = {
|
||||
bot: TProjectBots;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TProjectKeys } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
@@ -10,10 +12,11 @@ export const projectKeyDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findLatestProjectKey = async (
|
||||
userId: string,
|
||||
projectId: string
|
||||
projectId: string,
|
||||
tx?: Knex
|
||||
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
|
||||
try {
|
||||
const projectKey = await db(TableName.ProjectKeys)
|
||||
const projectKey = await (tx || db)(TableName.ProjectKeys)
|
||||
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
|
||||
.join(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`)
|
||||
.where({ projectId, receiverId: userId })
|
||||
@@ -29,9 +32,9 @@ export const projectKeyDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findAllProjectUserPubKeys = async (projectId: string) => {
|
||||
const findAllProjectUserPubKeys = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const pubKeys = await db(TableName.ProjectMembership)
|
||||
const pubKeys = await (tx || db)(TableName.ProjectMembership)
|
||||
.where({ projectId })
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TProjectMembershipDALFactory = ReturnType<typeof projectMembershipDALFactory>;
|
||||
|
||||
@@ -24,20 +24,63 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
||||
db.ref("role").withSchema(TableName.ProjectMembership),
|
||||
db.ref("roleId").withSchema(TableName.ProjectMembership),
|
||||
db.ref("isGhost").withSchema(TableName.Users),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId")
|
||||
);
|
||||
return members.map(({ email, firstName, lastName, publicKey, ...data }) => ({
|
||||
)
|
||||
.where({ isGhost: false });
|
||||
return members.map(({ email, firstName, lastName, publicKey, isGhost, ...data }) => ({
|
||||
...data,
|
||||
user: { email, firstName, lastName, id: data.userId, publicKey }
|
||||
user: { email, firstName, lastName, id: data.userId, publicKey, isGhost }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all project members" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...projectMemberOrm, findAllProjectMembers };
|
||||
const findProjectGhostUser = async (projectId: string) => {
|
||||
try {
|
||||
const ghostUser = await db(TableName.ProjectMembership)
|
||||
.where({ projectId })
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.select(selectAllTableCols(TableName.Users))
|
||||
.where({ isGhost: true })
|
||||
.first();
|
||||
|
||||
return ghostUser;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find project top-level user" });
|
||||
}
|
||||
};
|
||||
|
||||
const findMembershipsByEmail = async (projectId: string, emails: string[]) => {
|
||||
try {
|
||||
const members = await db(TableName.ProjectMembership)
|
||||
.where({ projectId })
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.join<TUserEncryptionKeys>(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.UserEncryptionKey}.userId`,
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.select(
|
||||
selectAllTableCols(TableName.ProjectMembership),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("email").withSchema(TableName.Users)
|
||||
)
|
||||
.whereIn("email", emails)
|
||||
.where({ isGhost: false });
|
||||
return members.map(({ userId, email, ...data }) => ({
|
||||
...data,
|
||||
user: { id: userId, email }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find members by email" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByEmail };
|
||||
};
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas";
|
||||
import {
|
||||
OrgMembershipStatus,
|
||||
ProjectMembershipRole,
|
||||
ProjectVersion,
|
||||
SecretKeyEncoding,
|
||||
TableName,
|
||||
TProjectMemberships,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
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 { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
@@ -17,7 +30,9 @@ import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||
import {
|
||||
TAddUsersToWorkspaceDTO,
|
||||
TDeleteProjectMembershipDTO,
|
||||
TAddUsersToWorkspaceNonE2EEDTO,
|
||||
TDeleteProjectMembershipOldDTO,
|
||||
TDeleteProjectMembershipsDTO,
|
||||
TGetProjectMembershipDTO,
|
||||
TInviteUserToProjectDTO,
|
||||
TUpdateProjectMembershipDTO
|
||||
@@ -26,11 +41,12 @@ import {
|
||||
type TProjectMembershipServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
smtpService: TSmtpService;
|
||||
projectBotDAL: TProjectBotDALFactory;
|
||||
projectMembershipDAL: TProjectMembershipDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne">;
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByEmail">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
@@ -42,6 +58,7 @@ export const projectMembershipServiceFactory = ({
|
||||
projectMembershipDAL,
|
||||
smtpService,
|
||||
projectRoleDAL,
|
||||
projectBotDAL,
|
||||
orgDAL,
|
||||
userDAL,
|
||||
projectDAL,
|
||||
@@ -55,64 +72,90 @@ export const projectMembershipServiceFactory = ({
|
||||
return projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
};
|
||||
|
||||
const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, email }: TInviteUserToProjectDTO) => {
|
||||
const inviteUserToProject = async ({ actorId, actor, actorOrgId, projectId, emails }: TInviteUserToProjectDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
|
||||
const invitee = await userDAL.findOne({ email });
|
||||
if (!invitee || !invitee.isAccepted)
|
||||
throw new BadRequestError({
|
||||
message: "Faield to validate invitee",
|
||||
name: "Invite user to project"
|
||||
});
|
||||
|
||||
const inviteeMembership = await projectMembershipDAL.findOne({
|
||||
userId: invitee.id,
|
||||
projectId
|
||||
});
|
||||
if (inviteeMembership)
|
||||
throw new BadRequestError({
|
||||
message: "Existing member of project",
|
||||
name: "Invite user to project"
|
||||
});
|
||||
const invitees: TUsers[] = [];
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const inviteeMembershipOrg = await orgDAL.findMembership({
|
||||
userId: invitee.id,
|
||||
orgId: project.orgId,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
const users = await userDAL.find({
|
||||
$in: { email: emails }
|
||||
});
|
||||
if (!inviteeMembershipOrg)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate invitee org membership",
|
||||
name: "Invite user to project"
|
||||
|
||||
await projectDAL.transaction(async (tx) => {
|
||||
for (const invitee of users) {
|
||||
if (!invitee.isAccepted)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate invitee",
|
||||
name: "Invite user to project"
|
||||
});
|
||||
|
||||
const inviteeMembership = await projectMembershipDAL.findOne(
|
||||
{
|
||||
userId: invitee.id,
|
||||
projectId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (inviteeMembership) {
|
||||
throw new BadRequestError({
|
||||
message: "Existing member of project",
|
||||
name: "Invite user to project"
|
||||
});
|
||||
}
|
||||
|
||||
const inviteeMembershipOrg = await orgDAL.findMembership({
|
||||
userId: invitee.id,
|
||||
orgId: project.orgId,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
});
|
||||
|
||||
if (!inviteeMembershipOrg) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate invitee org membership",
|
||||
name: "Invite user to project"
|
||||
});
|
||||
}
|
||||
|
||||
await projectMembershipDAL.create(
|
||||
{
|
||||
userId: invitee.id,
|
||||
projectId,
|
||||
role: ProjectMembershipRole.Member
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
invitees.push(invitee);
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: invitees.map((i) => i.email),
|
||||
substitutions: {
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const latestKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
|
||||
await projectMembershipDAL.create({
|
||||
userId: invitee.id,
|
||||
projectId,
|
||||
role: ProjectMembershipRole.Member
|
||||
});
|
||||
|
||||
const sender = await userDAL.findById(actorId);
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: [invitee.email],
|
||||
substitutions: {
|
||||
inviterFirstName: sender.firstName,
|
||||
inviterEmail: sender.email,
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
|
||||
return { invitee, latestKey };
|
||||
return { invitees, latestKey };
|
||||
};
|
||||
|
||||
const addUsersToProject = async ({ projectId, actorId, actor, actorOrgId, members }: TAddUsersToWorkspaceDTO) => {
|
||||
const addUsersToProject = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
members,
|
||||
sendEmails = true
|
||||
}: TAddUsersToWorkspaceDTO) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
@@ -134,11 +177,16 @@ export const projectMembershipServiceFactory = ({
|
||||
|
||||
await projectMembershipDAL.transaction(async (tx) => {
|
||||
await projectMembershipDAL.insertMany(
|
||||
orgMembers.map(({ userId }) => ({
|
||||
projectId,
|
||||
userId: userId as string,
|
||||
role: ProjectMembershipRole.Member
|
||||
})),
|
||||
orgMembers.map(({ userId, id: membershipId }) => {
|
||||
const role =
|
||||
members.find((i) => i.orgMembershipId === membershipId)?.projectRole || ProjectMembershipRole.Member;
|
||||
|
||||
return {
|
||||
projectId,
|
||||
userId: userId as string,
|
||||
role
|
||||
};
|
||||
}),
|
||||
tx
|
||||
);
|
||||
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
||||
@@ -153,22 +201,132 @@ export const projectMembershipServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
});
|
||||
const sender = await userDAL.findById(actorId);
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
|
||||
substitutions: {
|
||||
inviterFirstName: sender.firstName,
|
||||
inviterEmail: sender.email,
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
|
||||
if (sendEmails) {
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: orgMembers.map(({ email }) => email).filter(Boolean),
|
||||
substitutions: {
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
}
|
||||
return orgMembers;
|
||||
};
|
||||
|
||||
const addUsersToProjectNonE2EE = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
emails,
|
||||
sendEmails = true
|
||||
}: TAddUsersToWorkspaceNonE2EEDTO) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
if (project.version === ProjectVersion.V1) {
|
||||
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
|
||||
const orgMembers = await orgDAL.findOrgMembersByEmail(project.orgId, emails);
|
||||
|
||||
if (orgMembers.length !== emails.length) throw new BadRequestError({ message: "Some users are not part of org" });
|
||||
|
||||
const existingMembers = await projectMembershipDAL.find({
|
||||
projectId,
|
||||
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
|
||||
});
|
||||
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
||||
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
});
|
||||
}
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const newWsMembers = assignWorkspaceKeysToMembers({
|
||||
decryptKey: ghostUserLatestKey,
|
||||
userPrivateKey: botPrivateKey,
|
||||
members: orgMembers.map((membership) => ({
|
||||
orgMembershipId: membership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Member,
|
||||
userPublicKey: membership.user.publicKey
|
||||
}))
|
||||
});
|
||||
|
||||
const members: TProjectMemberships[] = [];
|
||||
|
||||
await projectMembershipDAL.transaction(async (tx) => {
|
||||
const result = await projectMembershipDAL.insertMany(
|
||||
orgMembers.map(({ user }) => ({
|
||||
projectId,
|
||||
userId: user.id,
|
||||
role: ProjectMembershipRole.Member
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
members.push(...result);
|
||||
|
||||
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||
await projectKeyDAL.insertMany(
|
||||
orgMembers.map(({ user, id }) => ({
|
||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: user.id,
|
||||
projectId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
if (sendEmails) {
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: orgMembers.map(({ user }) => user.email).filter(Boolean),
|
||||
substitutions: {
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
}
|
||||
return members;
|
||||
};
|
||||
|
||||
const updateProjectMembership = async ({
|
||||
actorId,
|
||||
actor,
|
||||
@@ -180,6 +338,15 @@ export const projectMembershipServiceFactory = ({
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
|
||||
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||
|
||||
if (membershipUser?.isGhost) {
|
||||
throw new BadRequestError({
|
||||
message: "Unauthorized member update",
|
||||
name: "Update project membership"
|
||||
});
|
||||
}
|
||||
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
||||
if (isCustomRole) {
|
||||
const customRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||
@@ -205,16 +372,26 @@ export const projectMembershipServiceFactory = ({
|
||||
return membership;
|
||||
};
|
||||
|
||||
// This is old and should be removed later. Its not used anywhere, but it is exposed in our API. So to avoid breaking changes, we are keeping it for now.
|
||||
const deleteProjectMembership = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
membershipId
|
||||
}: TDeleteProjectMembershipDTO) => {
|
||||
}: TDeleteProjectMembershipOldDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||
|
||||
const member = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||
|
||||
if (member?.isGhost) {
|
||||
throw new BadRequestError({
|
||||
message: "Unauthorized member delete",
|
||||
name: "Delete project membership"
|
||||
});
|
||||
}
|
||||
|
||||
const membership = await projectMembershipDAL.transaction(async (tx) => {
|
||||
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
|
||||
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
|
||||
@@ -223,11 +400,78 @@ export const projectMembershipServiceFactory = ({
|
||||
return membership;
|
||||
};
|
||||
|
||||
const deleteProjectMemberships = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
emails
|
||||
}: TDeleteProjectMembershipsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
|
||||
if (!project) {
|
||||
throw new BadRequestError({
|
||||
message: "Project not found",
|
||||
name: "Delete project membership"
|
||||
});
|
||||
}
|
||||
|
||||
if (project.version === ProjectVersion.V1) {
|
||||
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
|
||||
}
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findMembershipsByEmail(projectId, emails);
|
||||
|
||||
if (projectMembers.length !== emails.length) {
|
||||
throw new BadRequestError({
|
||||
message: "Some users are not part of project",
|
||||
name: "Delete project membership"
|
||||
});
|
||||
}
|
||||
|
||||
if (actor === ActorType.USER && projectMembers.some(({ user }) => user.id === actorId)) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot remove yourself from project",
|
||||
name: "Delete project membership"
|
||||
});
|
||||
}
|
||||
|
||||
const memberships = await projectMembershipDAL.transaction(async (tx) => {
|
||||
const deletedMemberships = await projectMembershipDAL.delete(
|
||||
{
|
||||
projectId,
|
||||
$in: {
|
||||
id: projectMembers.map(({ id }) => id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
projectId,
|
||||
$in: {
|
||||
receiverId: projectMembers.map(({ user }) => user.id).filter(Boolean)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return deletedMemberships;
|
||||
});
|
||||
return memberships;
|
||||
};
|
||||
|
||||
return {
|
||||
getProjectMemberships,
|
||||
inviteUserToProject,
|
||||
updateProjectMembership,
|
||||
deleteProjectMembership,
|
||||
addUsersToProjectNonE2EE,
|
||||
deleteProjectMemberships,
|
||||
deleteProjectMembership, // TODO: Remove this
|
||||
addUsersToProject
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetProjectMembershipDTO = TProjectPermission;
|
||||
|
||||
export type TInviteUserToProjectDTO = {
|
||||
email: string;
|
||||
emails: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateProjectMembershipDTO = {
|
||||
@@ -11,14 +12,25 @@ export type TUpdateProjectMembershipDTO = {
|
||||
role: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TDeleteProjectMembershipDTO = {
|
||||
export type TDeleteProjectMembershipOldDTO = {
|
||||
membershipId: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TDeleteProjectMembershipsDTO = {
|
||||
emails: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TAddUsersToWorkspaceDTO = {
|
||||
sendEmails?: boolean;
|
||||
members: {
|
||||
orgMembershipId: string;
|
||||
workspaceEncryptedKey: string;
|
||||
workspaceEncryptedNonce: string;
|
||||
projectRole: ProjectMembershipRole;
|
||||
}[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TAddUsersToWorkspaceNonE2EEDTO = {
|
||||
sendEmails?: boolean;
|
||||
emails: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { ProjectsSchema, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
|
||||
export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
|
||||
@@ -52,6 +54,32 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findProjectGhostUser = async (projectId: string) => {
|
||||
try {
|
||||
const ghostUser = await db(TableName.ProjectMembership)
|
||||
.where({ projectId })
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.select(selectAllTableCols(TableName.Users))
|
||||
.where({ isGhost: true })
|
||||
.first();
|
||||
return ghostUser;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find project top-level user" });
|
||||
}
|
||||
};
|
||||
|
||||
const setProjectUpgradeStatus = async (projectId: string, status: ProjectUpgradeStatus | null, tx?: Knex) => {
|
||||
try {
|
||||
const data: TProjectsUpdate = {
|
||||
upgradeStatus: status
|
||||
} as const;
|
||||
|
||||
await (tx || db)(TableName.Project).where({ id: projectId }).update(data);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Set project upgrade status" });
|
||||
}
|
||||
};
|
||||
|
||||
const findAllProjectsByIdentity = async (identityId: string) => {
|
||||
try {
|
||||
const workspaces = await db(TableName.IdentityProjectMembership)
|
||||
@@ -132,10 +160,25 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkProjectUpgradeStatus = async (projectId: string) => {
|
||||
const project = await projectOrm.findById(projectId);
|
||||
const upgradeInProgress =
|
||||
project.upgradeStatus === ProjectUpgradeStatus.InProgress && project.version === ProjectVersion.V1;
|
||||
|
||||
if (upgradeInProgress) {
|
||||
throw new BadRequestError({
|
||||
message: "Project is currently being upgraded, and secrets cannot be written. Please try again"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...projectOrm,
|
||||
findAllProjects,
|
||||
setProjectUpgradeStatus,
|
||||
findAllProjectsByIdentity,
|
||||
findProjectById
|
||||
findProjectGhostUser,
|
||||
findProjectById,
|
||||
checkProjectUpgradeStatus
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||
|
||||
import { AddUserToWsDTO } from "./project-types";
|
||||
|
||||
export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
|
||||
const plaintextProjectKey = decryptAsymmetric({
|
||||
ciphertext: decryptKey.encryptedKey,
|
||||
nonce: decryptKey.nonce,
|
||||
publicKey: decryptKey.sender.publicKey,
|
||||
privateKey: userPrivateKey
|
||||
});
|
||||
|
||||
const newWsMembers = members.map(({ orgMembershipId, userPublicKey, projectMembershipRole }) => {
|
||||
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAsymmetric(
|
||||
plaintextProjectKey,
|
||||
userPublicKey,
|
||||
userPrivateKey
|
||||
);
|
||||
|
||||
return {
|
||||
orgMembershipId,
|
||||
projectRole: projectMembershipRole,
|
||||
workspaceEncryptedKey: inviteeCipherText,
|
||||
workspaceEncryptedNonce: inviteeNonce
|
||||
};
|
||||
});
|
||||
|
||||
return newWsMembers;
|
||||
};
|
||||
|
||||
type TCreateProjectKeyDTO = {
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
export const createProjectKey = ({ publicKey, privateKey }: TCreateProjectKeyDTO) => {
|
||||
// 3. Create a random key that we'll use as the project key.
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
// 4. Encrypt the project key with the users key pair.
|
||||
const { ciphertext: encryptedProjectKey, nonce: encryptedProjectKeyIv } = encryptAsymmetric(
|
||||
randomBytes,
|
||||
publicKey,
|
||||
privateKey
|
||||
);
|
||||
|
||||
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
||||
};
|
||||
@@ -0,0 +1,541 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import {
|
||||
IntegrationAuthsSchema,
|
||||
ProjectMembershipRole,
|
||||
ProjectUpgradeStatus,
|
||||
ProjectVersion,
|
||||
SecretApprovalRequestsSecretsSchema,
|
||||
SecretKeyEncoding,
|
||||
SecretsSchema,
|
||||
SecretVersionsSchema,
|
||||
TIntegrationAuths,
|
||||
TSecretApprovalRequestsSecrets,
|
||||
TSecrets,
|
||||
TSecretVersions
|
||||
} from "@app/db/schemas";
|
||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||
import { RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||
import {
|
||||
decryptIntegrationAuths,
|
||||
decryptSecretApprovals,
|
||||
decryptSecrets,
|
||||
decryptSecretVersions
|
||||
} from "@app/lib/crypto";
|
||||
import {
|
||||
decryptAsymmetric,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
infisicalSymmetricDecrypt,
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueJobTypes, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectDALFactory } from "./project-dal";
|
||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
||||
|
||||
export type TProjectQueueFactory = ReturnType<typeof projectQueueFactory>;
|
||||
|
||||
type TProjectQueueFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "bulkUpdateNoVersionIncrement" | "delete">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "find">;
|
||||
secretDAL: Pick<TSecretDALFactory, "find" | "bulkUpdateNoVersionIncrement">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "find" | "create" | "delete" | "insertMany">;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "find">;
|
||||
secretApprovalSecretDAL: Pick<TSecretApprovalRequestSecretDALFactory, "find" | "bulkUpdateNoVersionIncrement">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "delete" | "create">;
|
||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
|
||||
integrationAuthDAL: TIntegrationAuthDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
||||
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "find">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "transaction" | "updateById" | "setProjectUpgradeStatus" | "find">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||
};
|
||||
|
||||
export const projectQueueFactory = ({
|
||||
queueService,
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
userDAL,
|
||||
secretVersionDAL,
|
||||
integrationAuthDAL,
|
||||
secretApprovalRequestDAL,
|
||||
secretApprovalSecretDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
projectEnvDAL,
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
orgService,
|
||||
projectMembershipDAL
|
||||
}: TProjectQueueFactoryDep) => {
|
||||
const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => {
|
||||
await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, {
|
||||
attempts: 1,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: {
|
||||
count: 5 // keep the most recent jobs
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
queueService.start(QueueName.UpgradeProjectToGhost, async ({ data }) => {
|
||||
try {
|
||||
const [project] = await projectDAL.find({
|
||||
id: data.projectId,
|
||||
version: ProjectVersion.V1
|
||||
});
|
||||
|
||||
const oldProjectKey = await projectKeyDAL.findLatestProjectKey(data.startedByUserId, data.projectId);
|
||||
|
||||
if (!project || !oldProjectKey) {
|
||||
throw new Error("Project or project key not found");
|
||||
}
|
||||
|
||||
if (project.upgradeStatus !== ProjectUpgradeStatus.Failed && project.upgradeStatus !== null) {
|
||||
throw new Error("Project upgrade status is not valid");
|
||||
}
|
||||
|
||||
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.InProgress); // Set the status to in progress. This is important to prevent multiple upgrades at the same time.
|
||||
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
// await new Promise((resolve) => setTimeout(resolve, 50_000));
|
||||
|
||||
const userPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: data.encryptedPrivateKey.keyEncoding,
|
||||
ciphertext: data.encryptedPrivateKey.encryptedKey,
|
||||
iv: data.encryptedPrivateKey.encryptedKeyIv,
|
||||
tag: data.encryptedPrivateKey.encryptedKeyTag
|
||||
});
|
||||
|
||||
const projectEnvs = await projectEnvDAL.find({
|
||||
projectId: project.id
|
||||
});
|
||||
|
||||
const projectFolders = await folderDAL.find({
|
||||
$in: {
|
||||
envId: projectEnvs.map((env) => env.id)
|
||||
}
|
||||
});
|
||||
|
||||
// Get all the secrets within the project (as encrypted)
|
||||
const projectIntegrationAuths = await integrationAuthDAL.find({
|
||||
projectId: project.id
|
||||
});
|
||||
const secrets: TSecrets[] = [];
|
||||
const secretVersions: TSecretVersions[] = [];
|
||||
const approvalSecrets: TSecretApprovalRequestsSecrets[] = [];
|
||||
const folderSecretVersionIdsToDelete: string[] = [];
|
||||
|
||||
for (const folder of projectFolders) {
|
||||
const folderSecrets = await secretDAL.find({ folderId: folder.id });
|
||||
|
||||
const folderSecretVersions = await secretVersionDAL.find(
|
||||
{
|
||||
folderId: folder.id
|
||||
},
|
||||
// Only get the latest 700 secret versions for each folder.
|
||||
{
|
||||
limit: 1000,
|
||||
sort: [["createdAt", "desc"]]
|
||||
}
|
||||
);
|
||||
|
||||
const deletedSecretVersions = await secretVersionDAL.find(
|
||||
{
|
||||
folderId: folder.id
|
||||
},
|
||||
{
|
||||
// Get all the secret versions that are not the latest 700
|
||||
offset: 1000
|
||||
}
|
||||
);
|
||||
folderSecretVersionIdsToDelete.push(...deletedSecretVersions.map((el) => el.id));
|
||||
|
||||
const approvalRequests = await secretApprovalRequestDAL.find({
|
||||
status: RequestState.Open,
|
||||
folderId: folder.id
|
||||
});
|
||||
const secretApprovals = await secretApprovalSecretDAL.find({
|
||||
$in: {
|
||||
requestId: approvalRequests.map((el) => el.id)
|
||||
}
|
||||
});
|
||||
|
||||
secrets.push(...folderSecrets);
|
||||
secretVersions.push(...folderSecretVersions);
|
||||
approvalSecrets.push(...secretApprovals);
|
||||
}
|
||||
|
||||
const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey);
|
||||
const decryptedSecretVersions = decryptSecretVersions(secretVersions, userPrivateKey, oldProjectKey);
|
||||
const decryptedApprovalSecrets = decryptSecretApprovals(approvalSecrets, userPrivateKey, oldProjectKey);
|
||||
const decryptedIntegrationAuths = decryptIntegrationAuths(projectIntegrationAuths, userPrivateKey, oldProjectKey);
|
||||
|
||||
// Get the existing bot and the existing project keys for the members of the project
|
||||
const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null);
|
||||
const existingProjectKeys = await projectKeyDAL.find({ projectId: project.id });
|
||||
|
||||
// TRANSACTION START
|
||||
await projectDAL.transaction(async (tx) => {
|
||||
await projectDAL.updateById(project.id, { version: ProjectVersion.V2 }, tx);
|
||||
|
||||
// Create a ghost user
|
||||
const ghostUser = await orgService.addGhostUser(project.orgId, tx);
|
||||
|
||||
// Create a project key
|
||||
const { key: newEncryptedProjectKey, iv: newEncryptedProjectKeyIv } = createProjectKey({
|
||||
publicKey: ghostUser.keys.publicKey,
|
||||
privateKey: ghostUser.keys.plainPrivateKey
|
||||
});
|
||||
|
||||
// Create a new project key for the GHOST
|
||||
await projectKeyDAL.create(
|
||||
{
|
||||
projectId: project.id,
|
||||
receiverId: ghostUser.user.id,
|
||||
encryptedKey: newEncryptedProjectKey,
|
||||
nonce: newEncryptedProjectKeyIv,
|
||||
senderId: ghostUser.user.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// Create a membership for the ghost user
|
||||
await projectMembershipDAL.create(
|
||||
{
|
||||
projectId: project.id,
|
||||
userId: ghostUser.user.id,
|
||||
role: ProjectMembershipRole.Admin
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// If a bot already exists, delete it
|
||||
if (existingBot) {
|
||||
await projectBotDAL.delete({ id: existingBot.id }, tx);
|
||||
}
|
||||
|
||||
// Delete all the existing project keys
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
projectId: project.id,
|
||||
$in: {
|
||||
id: existingProjectKeys.map((key) => key.id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
||||
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new Error("User latest key not found (V2 Upgrade)");
|
||||
}
|
||||
|
||||
const newProjectMembers: {
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
senderId: string;
|
||||
receiverId: string;
|
||||
projectId: string;
|
||||
}[] = [];
|
||||
|
||||
for (const key of existingProjectKeys) {
|
||||
const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
|
||||
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
|
||||
|
||||
if (!user || !orgMembership) {
|
||||
throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`);
|
||||
}
|
||||
|
||||
const [newMember] = assignWorkspaceKeysToMembers({
|
||||
decryptKey: ghostUserLatestKey,
|
||||
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
||||
members: [
|
||||
{
|
||||
userPublicKey: user.publicKey,
|
||||
orgMembershipId: orgMembership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
newProjectMembers.push({
|
||||
encryptedKey: newMember.workspaceEncryptedKey,
|
||||
nonce: newMember.workspaceEncryptedNonce,
|
||||
senderId: ghostUser.user.id,
|
||||
receiverId: user.id,
|
||||
projectId: project.id
|
||||
});
|
||||
}
|
||||
|
||||
// Create project keys for all the old members
|
||||
await projectKeyDAL.insertMany(newProjectMembers, tx);
|
||||
|
||||
// Encrypt the bot private key (which is the same as the ghost user)
|
||||
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
||||
|
||||
// 5. Create a bot for the project
|
||||
const newBot = await projectBotDAL.create(
|
||||
{
|
||||
name: "Infisical Bot (Ghost)",
|
||||
projectId: project.id,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: true,
|
||||
publicKey: ghostUser.keys.publicKey,
|
||||
senderId: ghostUser.user.id,
|
||||
encryptedProjectKey: newEncryptedProjectKey,
|
||||
encryptedProjectKeyNonce: newEncryptedProjectKeyIv,
|
||||
algorithm,
|
||||
keyEncoding: encoding
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: newBot.keyEncoding as SecretKeyEncoding,
|
||||
iv: newBot.iv,
|
||||
tag: newBot.tag,
|
||||
ciphertext: newBot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const botKey = decryptAsymmetric({
|
||||
ciphertext: newBot.encryptedProjectKey!,
|
||||
privateKey: botPrivateKey,
|
||||
nonce: newBot.encryptedProjectKeyNonce!,
|
||||
publicKey: ghostUser.keys.publicKey
|
||||
});
|
||||
|
||||
const updatedSecrets: TSecrets[] = [];
|
||||
const updatedSecretVersions: TSecretVersions[] = [];
|
||||
const updatedSecretApprovals: TSecretApprovalRequestsSecrets[] = [];
|
||||
const updatedIntegrationAuths: TIntegrationAuths[] = [];
|
||||
for (const rawSecret of decryptedSecrets) {
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.decrypted.secretKey, botKey);
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.decrypted.secretValue || "", botKey);
|
||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(
|
||||
rawSecret.decrypted.secretComment || "",
|
||||
botKey
|
||||
);
|
||||
|
||||
const payload: TSecrets = {
|
||||
...rawSecret.original,
|
||||
keyEncoding: SecretKeyEncoding.UTF8,
|
||||
|
||||
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||
secretKeyIV: secretKeyEncrypted.iv,
|
||||
secretKeyTag: secretKeyEncrypted.tag,
|
||||
|
||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||
secretValueIV: secretValueEncrypted.iv,
|
||||
secretValueTag: secretValueEncrypted.tag,
|
||||
|
||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||
secretCommentIV: secretCommentEncrypted.iv,
|
||||
secretCommentTag: secretCommentEncrypted.tag
|
||||
} as const;
|
||||
|
||||
if (!SecretsSchema.safeParse(payload).success) {
|
||||
throw new Error(`Invalid secret payload: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
|
||||
updatedSecrets.push(payload);
|
||||
}
|
||||
|
||||
for (const rawSecretVersion of decryptedSecretVersions) {
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecretVersion.decrypted.secretKey, botKey);
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(
|
||||
rawSecretVersion.decrypted.secretValue || "",
|
||||
botKey
|
||||
);
|
||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(
|
||||
rawSecretVersion.decrypted.secretComment || "",
|
||||
botKey
|
||||
);
|
||||
|
||||
const payload: TSecretVersions = {
|
||||
...rawSecretVersion.original,
|
||||
keyEncoding: SecretKeyEncoding.UTF8,
|
||||
|
||||
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||
secretKeyIV: secretKeyEncrypted.iv,
|
||||
secretKeyTag: secretKeyEncrypted.tag,
|
||||
|
||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||
secretValueIV: secretValueEncrypted.iv,
|
||||
secretValueTag: secretValueEncrypted.tag,
|
||||
|
||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||
secretCommentIV: secretCommentEncrypted.iv,
|
||||
secretCommentTag: secretCommentEncrypted.tag
|
||||
} as const;
|
||||
|
||||
if (!SecretVersionsSchema.safeParse(payload).success) {
|
||||
throw new Error(`Invalid secret version payload: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
|
||||
updatedSecretVersions.push(payload);
|
||||
}
|
||||
|
||||
for (const rawSecretApproval of decryptedApprovalSecrets) {
|
||||
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecretApproval.decrypted.secretKey, botKey);
|
||||
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(
|
||||
rawSecretApproval.decrypted.secretValue || "",
|
||||
botKey
|
||||
);
|
||||
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(
|
||||
rawSecretApproval.decrypted.secretComment || "",
|
||||
botKey
|
||||
);
|
||||
|
||||
const payload: TSecretApprovalRequestsSecrets = {
|
||||
...rawSecretApproval.original,
|
||||
keyEncoding: SecretKeyEncoding.UTF8,
|
||||
|
||||
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||
secretKeyIV: secretKeyEncrypted.iv,
|
||||
secretKeyTag: secretKeyEncrypted.tag,
|
||||
|
||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||
secretValueIV: secretValueEncrypted.iv,
|
||||
secretValueTag: secretValueEncrypted.tag,
|
||||
|
||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||
secretCommentIV: secretCommentEncrypted.iv,
|
||||
secretCommentTag: secretCommentEncrypted.tag
|
||||
} as const;
|
||||
|
||||
if (!SecretApprovalRequestsSecretsSchema.safeParse(payload).success) {
|
||||
throw new Error(`Invalid secret approval payload: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
|
||||
updatedSecretApprovals.push(payload);
|
||||
}
|
||||
|
||||
for (const integrationAuth of decryptedIntegrationAuths) {
|
||||
const access = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.access, botKey);
|
||||
const accessId = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.accessId, botKey);
|
||||
const refresh = encryptSymmetric128BitHexKeyUTF8(integrationAuth.decrypted.refresh, botKey);
|
||||
|
||||
const payload: TIntegrationAuths = {
|
||||
...integrationAuth.original,
|
||||
keyEncoding: SecretKeyEncoding.UTF8,
|
||||
|
||||
accessCiphertext: access.ciphertext,
|
||||
accessIV: access.iv,
|
||||
accessTag: access.tag,
|
||||
|
||||
accessIdCiphertext: accessId.ciphertext,
|
||||
accessIdIV: accessId.iv,
|
||||
accessIdTag: accessId.tag,
|
||||
|
||||
refreshCiphertext: refresh.ciphertext,
|
||||
refreshIV: refresh.iv,
|
||||
refreshTag: refresh.tag
|
||||
} as const;
|
||||
|
||||
if (!IntegrationAuthsSchema.safeParse(payload).success) {
|
||||
throw new Error(`Invalid integration auth payload: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
|
||||
updatedIntegrationAuths.push(payload);
|
||||
}
|
||||
|
||||
if (updatedSecrets.length !== secrets.length) {
|
||||
throw new Error("Failed to update some secrets");
|
||||
}
|
||||
if (updatedSecretVersions.length !== secretVersions.length) {
|
||||
throw new Error("Failed to update some secret versions");
|
||||
}
|
||||
if (updatedSecretApprovals.length !== approvalSecrets.length) {
|
||||
throw new Error("Failed to update some secret approvals");
|
||||
}
|
||||
if (updatedIntegrationAuths.length !== projectIntegrationAuths.length) {
|
||||
throw new Error("Failed to update some integration auths");
|
||||
}
|
||||
|
||||
const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(updatedSecrets, tx);
|
||||
const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(updatedSecretVersions, tx);
|
||||
const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement(
|
||||
updatedSecretApprovals,
|
||||
tx
|
||||
);
|
||||
const integrationAuthUpdates = await integrationAuthDAL.bulkUpdate(
|
||||
updatedIntegrationAuths.map((el) => ({
|
||||
filter: { id: el.id },
|
||||
data: {
|
||||
...el,
|
||||
id: undefined
|
||||
}
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
// Delete all secret versions that are no longer needed. We only store the latest 100 versions for each secret.
|
||||
await secretVersionDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
id: folderSecretVersionIdsToDelete
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (
|
||||
secretUpdates.length !== updatedSecrets.length ||
|
||||
secretVersionUpdates.length !== updatedSecretVersions.length ||
|
||||
secretApprovalUpdates.length !== updatedSecretApprovals.length ||
|
||||
integrationAuthUpdates.length !== updatedIntegrationAuths.length
|
||||
) {
|
||||
throw new Error("Parts of the upgrade failed. Some secrets were not updated");
|
||||
}
|
||||
|
||||
await projectDAL.setProjectUpgradeStatus(data.projectId, null, tx);
|
||||
|
||||
// await new Promise((resolve) => setTimeout(resolve, 15_000));
|
||||
// throw new Error("Transaction was successful!");
|
||||
});
|
||||
} catch (err) {
|
||||
const [project] = await projectDAL
|
||||
.find({
|
||||
id: data.projectId,
|
||||
version: ProjectVersion.V1
|
||||
})
|
||||
.catch(() => [null]);
|
||||
|
||||
if (!project) {
|
||||
logger.error("Failed to upgrade project, because no project was found", data);
|
||||
} else {
|
||||
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
|
||||
logger.error(err, "Failed to upgrade project");
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
queueService.listen(QueueName.UpgradeProjectToGhost, "failed", (job, err) => {
|
||||
logger.error(err, "Upgrade project failed", job?.data);
|
||||
});
|
||||
|
||||
return {
|
||||
upgradeProject
|
||||
};
|
||||
};
|
||||
@@ -1,22 +1,40 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
|
||||
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";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectDALFactory } from "./project-dal";
|
||||
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
|
||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
||||
import { TProjectQueueFactory } from "./project-queue";
|
||||
import {
|
||||
TCreateProjectDTO,
|
||||
TDeleteProjectDTO,
|
||||
TGetProjectDTO,
|
||||
TUpdateProjectDTO,
|
||||
TUpgradeProjectDTO
|
||||
} from "./project-types";
|
||||
|
||||
export const DEFAULT_PROJECT_ENVS = [
|
||||
{ name: "Development", slug: "dev" },
|
||||
@@ -26,11 +44,18 @@ export const DEFAULT_PROJECT_ENVS = [
|
||||
|
||||
type TProjectServiceFactoryDep = {
|
||||
projectDAL: TProjectDALFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "insertMany">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
|
||||
projectQueue: TProjectQueueFactory;
|
||||
userDAL: TUserDALFactory;
|
||||
folderDAL: TSecretFolderDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||
identityProjectDAL: TIdentityProjectDALFactory;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "create" | "findById" | "delete" | "findOne">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne">;
|
||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
@@ -38,8 +63,15 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||
|
||||
export const projectServiceFactory = ({
|
||||
projectDAL,
|
||||
projectQueue,
|
||||
projectKeyDAL,
|
||||
permissionService,
|
||||
userDAL,
|
||||
folderDAL,
|
||||
orgService,
|
||||
identityProjectDAL,
|
||||
projectBotDAL,
|
||||
identityOrgMembershipDAL,
|
||||
secretBlindIndexDAL,
|
||||
projectMembershipDAL,
|
||||
projectEnvDAL,
|
||||
@@ -48,8 +80,13 @@ export const projectServiceFactory = ({
|
||||
/*
|
||||
* Create workspace. Make user the admin
|
||||
* */
|
||||
const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName }: TCreateProjectDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgId);
|
||||
const createProject = async ({ orgId, actor, actorId, actorOrgId, workspaceName, slug }: TCreateProjectDTO) => {
|
||||
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
|
||||
const appCfg = getConfig();
|
||||
@@ -64,20 +101,28 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const newProject = projectDAL.transaction(async (tx) => {
|
||||
const results = await projectDAL.transaction(async (tx) => {
|
||||
const ghostUser = await orgService.addGhostUser(orgId, tx);
|
||||
|
||||
const project = await projectDAL.create(
|
||||
{ name: workspaceName, orgId, slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`) },
|
||||
{
|
||||
name: workspaceName,
|
||||
orgId,
|
||||
slug: slug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||
version: ProjectVersion.V2
|
||||
},
|
||||
tx
|
||||
);
|
||||
// set user as admin member for proeject
|
||||
// set ghost user as admin of project
|
||||
await projectMembershipDAL.create(
|
||||
{
|
||||
userId: actorId,
|
||||
userId: ghostUser.user.id,
|
||||
role: ProjectMembershipRole.Admin,
|
||||
projectId: project.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// generate the blind index for project
|
||||
await secretBlindIndexDAL.create(
|
||||
{
|
||||
@@ -99,18 +144,165 @@ export const projectServiceFactory = ({
|
||||
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
||||
tx
|
||||
);
|
||||
// _id for backward compat
|
||||
return { ...project, environments: envs, _id: project.id };
|
||||
|
||||
// 3. Create a random key that we'll use as the project key.
|
||||
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
|
||||
publicKey: ghostUser.keys.publicKey,
|
||||
privateKey: ghostUser.keys.plainPrivateKey
|
||||
});
|
||||
|
||||
// 4. Save the project key for the ghost user.
|
||||
await projectKeyDAL.create(
|
||||
{
|
||||
projectId: project.id,
|
||||
receiverId: ghostUser.user.id,
|
||||
encryptedKey: encryptedProjectKey,
|
||||
nonce: encryptedProjectKeyIv,
|
||||
senderId: ghostUser.user.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
||||
|
||||
// 5. Create & a bot for the project
|
||||
await projectBotDAL.create(
|
||||
{
|
||||
name: "Infisical Bot (Ghost)",
|
||||
projectId: project.id,
|
||||
tag,
|
||||
iv,
|
||||
encryptedProjectKey,
|
||||
encryptedProjectKeyNonce: encryptedProjectKeyIv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: true,
|
||||
publicKey: ghostUser.keys.publicKey,
|
||||
senderId: ghostUser.user.id,
|
||||
algorithm,
|
||||
keyEncoding: encoding
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// Find the ghost users latest key
|
||||
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
||||
|
||||
if (!latestKey) {
|
||||
throw new Error("Latest key not found for user");
|
||||
}
|
||||
|
||||
// If the project is being created by a user, add the user to the project as an admin
|
||||
if (actor === ActorType.USER) {
|
||||
// Find public key of user
|
||||
const user = await userDAL.findUserEncKeyByUserId(actorId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const [projectAdmin] = assignWorkspaceKeysToMembers({
|
||||
decryptKey: latestKey,
|
||||
userPrivateKey: ghostUser.keys.plainPrivateKey,
|
||||
members: [
|
||||
{
|
||||
userPublicKey: user.publicKey,
|
||||
orgMembershipId: orgMembership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Admin
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Create a membership for the user
|
||||
await projectMembershipDAL.create(
|
||||
{
|
||||
projectId: project.id,
|
||||
userId: user.id,
|
||||
role: projectAdmin.projectRole
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// Create a project key for the user
|
||||
await projectKeyDAL.create(
|
||||
{
|
||||
encryptedKey: projectAdmin.workspaceEncryptedKey,
|
||||
nonce: projectAdmin.workspaceEncryptedNonce,
|
||||
senderId: ghostUser.user.id,
|
||||
receiverId: user.id,
|
||||
projectId: project.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
// If the project is being created by an identity, add the identity to the project as an admin
|
||||
else if (actor === ActorType.IDENTITY) {
|
||||
// Find identity org membership
|
||||
const identityOrgMembership = await identityOrgMembershipDAL.findOne(
|
||||
{
|
||||
identityId: actorId,
|
||||
orgId: project.orgId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// If identity org membership not found, throw error
|
||||
if (!identityOrgMembership) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to find identity with id ${actorId}`
|
||||
});
|
||||
}
|
||||
|
||||
// Get the role permission for the identity
|
||||
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||
ProjectMembershipRole.Admin,
|
||||
orgId
|
||||
);
|
||||
|
||||
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPrivilege)
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Failed to add identity to project with more privileged role"
|
||||
});
|
||||
const isCustomRole = Boolean(customRole);
|
||||
|
||||
await identityProjectDAL.create(
|
||||
{
|
||||
identityId: actorId,
|
||||
projectId: project.id,
|
||||
role: isCustomRole ? ProjectMembershipRole.Custom : ProjectMembershipRole.Admin,
|
||||
roleId: customRole?.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...project,
|
||||
environments: envs,
|
||||
_id: project.id
|
||||
};
|
||||
});
|
||||
|
||||
return newProject;
|
||||
return results;
|
||||
};
|
||||
|
||||
const deleteProject = async ({ actor, actorId, actorOrgId, projectId }: TDeleteProjectDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||
|
||||
const deletedProject = await projectDAL.deleteById(projectId);
|
||||
const deletedProject = await projectDAL.transaction(async (tx) => {
|
||||
const project = await projectDAL.deleteById(projectId, tx);
|
||||
const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(projectId).catch(() => null);
|
||||
|
||||
// Delete the org membership for the ghost user if it's found.
|
||||
if (projectGhostUser) {
|
||||
await userDAL.deleteById(projectGhostUser.id, tx);
|
||||
}
|
||||
|
||||
return project;
|
||||
});
|
||||
|
||||
return deletedProject;
|
||||
};
|
||||
|
||||
@@ -124,6 +316,17 @@ export const projectServiceFactory = ({
|
||||
return projectDAL.findProjectById(projectId);
|
||||
};
|
||||
|
||||
const updateProject = async ({ projectId, actor, actorId, actorOrgId, update }: TUpdateProjectDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
const updatedProject = await projectDAL.updateById(projectId, {
|
||||
name: update.name,
|
||||
autoCapitalization: update.autoCapitalization
|
||||
});
|
||||
return updatedProject;
|
||||
};
|
||||
|
||||
const toggleAutoCapitalization = async ({
|
||||
projectId,
|
||||
actor,
|
||||
@@ -146,12 +349,55 @@ export const projectServiceFactory = ({
|
||||
return updatedProject;
|
||||
};
|
||||
|
||||
const upgradeProject = async ({ projectId, actor, actorId, userPrivateKey }: TUpgradeProjectDTO) => {
|
||||
const { permission, membership } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||
|
||||
if (membership?.role !== ProjectMembershipRole.Admin) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "User must be admin"
|
||||
});
|
||||
}
|
||||
|
||||
const encryptedPrivateKey = infisicalSymmetricEncypt(userPrivateKey);
|
||||
|
||||
await projectQueue.upgradeProject({
|
||||
projectId,
|
||||
startedByUserId: actorId,
|
||||
encryptedPrivateKey: {
|
||||
encryptedKey: encryptedPrivateKey.ciphertext,
|
||||
encryptedKeyIv: encryptedPrivateKey.iv,
|
||||
encryptedKeyTag: encryptedPrivateKey.tag,
|
||||
keyEncoding: encryptedPrivateKey.encoding
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getProjectUpgradeStatus = async ({ projectId, actor, actorId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
|
||||
if (!project) {
|
||||
throw new BadRequestError({
|
||||
message: `Project with id ${projectId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
return project.upgradeStatus || null;
|
||||
};
|
||||
|
||||
return {
|
||||
createProject,
|
||||
deleteProject,
|
||||
getProjects,
|
||||
updateProject,
|
||||
getProjectUpgradeStatus,
|
||||
getAProject,
|
||||
toggleAutoCapitalization,
|
||||
updateName
|
||||
updateName,
|
||||
upgradeProject
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { ProjectMembershipRole, TProjectKeys } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
|
||||
export type TCreateProjectDTO = {
|
||||
@@ -6,6 +9,7 @@ export type TCreateProjectDTO = {
|
||||
actorOrgId?: string;
|
||||
orgId: string;
|
||||
workspaceName: string;
|
||||
slug?: string;
|
||||
};
|
||||
|
||||
export type TDeleteProjectDTO = {
|
||||
@@ -21,3 +25,24 @@ export type TGetProjectDTO = {
|
||||
actorOrgId?: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TUpdateProjectDTO = {
|
||||
update: {
|
||||
name?: string;
|
||||
autoCapitalization?: boolean;
|
||||
};
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpgradeProjectDTO = {
|
||||
userPrivateKey: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type AddUserToWsDTO = {
|
||||
decryptKey: TProjectKeys & { sender: { publicKey: string } };
|
||||
userPrivateKey: string;
|
||||
members: {
|
||||
orgMembershipId: string;
|
||||
projectMembershipRole: ProjectMembershipRole;
|
||||
userPublicKey: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -41,13 +41,12 @@ export const fnSecretsFromImports = async ({
|
||||
environment: importEnv.slug,
|
||||
environmentInfo: importEnv,
|
||||
folderId: importedFolders?.[i]?.id,
|
||||
secrets: importedFolders?.[i]?.id
|
||||
? importedSecsGroupByFolderId[importedFolders?.[i]?.id as string].map((item) => ({
|
||||
...item,
|
||||
environment: importEnv.slug,
|
||||
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||
}))
|
||||
: []
|
||||
// this will ensure for cases when secrets are empty. Could be due to missing folder for a path or when emtpy secrets inside a given path
|
||||
secrets: (importedSecsGroupByFolderId?.[importedFolders?.[i]?.id as string] || []).map((item) => ({
|
||||
...item,
|
||||
environment: importEnv.slug,
|
||||
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||
}))
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
@@ -21,6 +22,7 @@ type TSecretImportServiceFactoryDep = {
|
||||
secretImportDAL: TSecretImportDALFactory;
|
||||
folderDAL: TSecretFolderDALFactory;
|
||||
secretDAL: Pick<TSecretDALFactory, "find">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||
projectEnvDAL: TProjectEnvDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
@@ -34,6 +36,7 @@ export const secretImportServiceFactory = ({
|
||||
projectEnvDAL,
|
||||
permissionService,
|
||||
folderDAL,
|
||||
projectDAL,
|
||||
secretDAL
|
||||
}: TSecretImportServiceFactoryDep) => {
|
||||
const createImport = async ({
|
||||
@@ -62,6 +65,8 @@ export const secretImportServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
|
||||
// the idea is to use postgres specific function
|
||||
// insert with id this will cause a conflict then merge the data
|
||||
const bulkUpdate = async (data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>, tx?: Knex) => {
|
||||
const bulkUpdate = async (
|
||||
data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>,
|
||||
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const secs = await Promise.all(
|
||||
data.map(async ({ filter, data: updateData }) => {
|
||||
@@ -41,6 +45,35 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const bulkUpdateNoVersionIncrement = async (data: TSecrets[], tx?: Knex) => {
|
||||
try {
|
||||
const existingSecrets = await secretOrm.find(
|
||||
{
|
||||
$in: {
|
||||
id: data.map((el) => el.id)
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (existingSecrets.length !== data.length) {
|
||||
throw new BadRequestError({ message: "Some of the secrets do not exist" });
|
||||
}
|
||||
|
||||
if (data.length === 0) return [];
|
||||
|
||||
const updatedSecrets = await (tx || db)(TableName.Secret)
|
||||
.insert(data)
|
||||
.onConflict("id") // this will cause a conflict then merge the data
|
||||
.merge() // Merge the data with the existing data
|
||||
.returning("*");
|
||||
|
||||
return updatedSecrets;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMany = async (
|
||||
data: Array<{ blindIndex: string; type: SecretType }>,
|
||||
folderId: string,
|
||||
@@ -57,6 +90,12 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
type: el.type,
|
||||
...(el.type === SecretType.Personal ? { userId } : {})
|
||||
});
|
||||
if (el.type === SecretType.Shared) {
|
||||
void bd.orWhere({
|
||||
secretBlindIndex: el.blindIndex,
|
||||
type: SecretType.Personal
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.delete()
|
||||
@@ -139,5 +178,13 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretOrm, update, bulkUpdate, deleteMany, findByFolderId, findByBlindIndexes };
|
||||
return {
|
||||
...secretOrm,
|
||||
update,
|
||||
bulkUpdate,
|
||||
deleteMany,
|
||||
bulkUpdateNoVersionIncrement,
|
||||
findByFolderId,
|
||||
findByBlindIndexes
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { groupBy, pick } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
@@ -49,6 +50,7 @@ type TSecretServiceFactoryDep = {
|
||||
secretTagDAL: TSecretTagDALFactory;
|
||||
secretVersionDAL: TSecretVersionDALFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
@@ -68,6 +70,7 @@ export const secretServiceFactory = ({
|
||||
permissionService,
|
||||
snapshotService,
|
||||
secretQueueService,
|
||||
projectDAL,
|
||||
projectBotService,
|
||||
secretImportDAL,
|
||||
secretVersionTagDAL
|
||||
@@ -281,6 +284,8 @@ export const secretServiceFactory = ({
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||
const folderId = folder.id;
|
||||
@@ -359,6 +364,8 @@ export const secretServiceFactory = ({
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||
const folderId = folder.id;
|
||||
@@ -459,6 +466,8 @@ export const secretServiceFactory = ({
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||
const folderId = folder.id;
|
||||
@@ -650,6 +659,8 @@ export const secretServiceFactory = ({
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||
const folderId = folder.id;
|
||||
@@ -705,6 +716,8 @@ export const secretServiceFactory = ({
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||
const folderId = folder.id;
|
||||
@@ -776,6 +789,8 @@ export const secretServiceFactory = ({
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
|
||||
await projectDAL.checkProjectUpgradeStatus(projectId);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||
const folderId = folder.id;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TSecretVersions } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
|
||||
@@ -36,6 +36,57 @@ export const secretVersionDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const bulkUpdate = async (
|
||||
data: Array<{ filter: Partial<TSecretVersions>; data: TSecretVersionsUpdate }>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const secs = await Promise.all(
|
||||
data.map(async ({ filter, data: updateData }) => {
|
||||
const [doc] = await (tx || db)(TableName.SecretVersion)
|
||||
.where(filter)
|
||||
.update(updateData)
|
||||
.increment("version", 1) // TODO: Is this really needed?
|
||||
.returning("*");
|
||||
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
|
||||
return doc;
|
||||
})
|
||||
);
|
||||
return secs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||
}
|
||||
};
|
||||
|
||||
const bulkUpdateNoVersionIncrement = async (data: TSecretVersions[], tx?: Knex) => {
|
||||
try {
|
||||
const existingSecretVersions = await secretVersionOrm.find(
|
||||
{
|
||||
$in: {
|
||||
id: data.map((el) => el.id)
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (existingSecretVersions.length !== data.length) {
|
||||
throw new BadRequestError({ message: "Some of the secret versions do not exist" });
|
||||
}
|
||||
|
||||
if (data.length === 0) return [];
|
||||
|
||||
const updatedSecretVersions = await (tx || db)(TableName.SecretVersion)
|
||||
.insert(data)
|
||||
.onConflict("id") // this will cause a conflict then merge the data
|
||||
.merge() // Merge the data with the existing data
|
||||
.returning("*");
|
||||
|
||||
return updatedSecretVersions;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "bulk update secret" });
|
||||
}
|
||||
};
|
||||
|
||||
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
|
||||
try {
|
||||
const docs: Array<TSecretVersions & { max: number }> = await (tx || db)(TableName.SecretVersion)
|
||||
@@ -59,5 +110,11 @@ export const secretVersionDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretVersionOrm, findLatestVersionMany, findLatestVersionByFolderId };
|
||||
return {
|
||||
...secretVersionOrm,
|
||||
findLatestVersionMany,
|
||||
bulkUpdate,
|
||||
findLatestVersionByFolderId,
|
||||
bulkUpdateNoVersionIncrement
|
||||
};
|
||||
};
|
||||
|
||||
@@ -25,7 +25,8 @@ export enum SmtpTemplates {
|
||||
OrgInvite = "organizationInvitation.handlebars",
|
||||
ResetPassword = "passwordReset.handlebars",
|
||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||
WorkspaceInvite = "workspaceInvitation.handlebars"
|
||||
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||
ScimUserProvisioned = "scimUserProvisioned.handlebars"
|
||||
}
|
||||
|
||||
export enum SmtpHost {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Organization Invitation</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Join your organization on Infisical</h2>
|
||||
<p>You've been invited to join the Infisical organization — {{organizationName}}</p>
|
||||
<a href="{{callback_url}}">Join now</a>
|
||||
<h3>What is Infisical?</h3>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,15 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Project Invitation</title>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Join your team on Infisical</h2>
|
||||
<p>{{inviterFirstName}} ({{inviterEmail}}) has invited you to their Infisical project — {{workspaceName}}</p>
|
||||
<p>You have been invited to a new Infisical project — {{workspaceName}}</p>
|
||||
<a href="{{callback_url}}">Join now</a>
|
||||
<h3>What is Infisical?</h3>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||
</body>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
|
||||
and configs.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -70,6 +70,7 @@ export const superAdminServiceFactory = ({
|
||||
lastName,
|
||||
email,
|
||||
superAdmin: true,
|
||||
isGhost: false,
|
||||
isAccepted: true,
|
||||
authMethods: [AuthMethod.EMAIL]
|
||||
},
|
||||
@@ -96,7 +97,11 @@ export const superAdminServiceFactory = ({
|
||||
|
||||
const initialOrganizationName = appCfg.INITIAL_ORGANIZATION_NAME ?? "Admin Org";
|
||||
|
||||
await orgService.createOrganization(userInfo.user.id, userInfo.user.email, initialOrganizationName);
|
||||
const organization = await orgService.createOrganization(
|
||||
userInfo.user.id,
|
||||
userInfo.user.email,
|
||||
initialOrganizationName
|
||||
);
|
||||
|
||||
await updateServerCfg({ initialized: true });
|
||||
const token = await authService.generateUserTokens({
|
||||
@@ -106,7 +111,7 @@ export const superAdminServiceFactory = ({
|
||||
organizationId: undefined
|
||||
});
|
||||
// TODO(akhilmhdh-pg): telemetry service
|
||||
return { token, user: userInfo };
|
||||
return { token, user: userInfo, organization };
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -23,7 +23,7 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
const findUserEncKeyByEmail = async (email: string) => {
|
||||
try {
|
||||
return await db(TableName.Users)
|
||||
.where({ email })
|
||||
.where({ email, isGhost: false })
|
||||
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`)
|
||||
.first();
|
||||
} catch (error) {
|
||||
@@ -47,6 +47,17 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findUserByProjectMembershipId = async (projectMembershipId: string) => {
|
||||
try {
|
||||
return await db(TableName.ProjectMembership)
|
||||
.where({ [`${TableName.ProjectMembership}.id` as "id"]: projectMembershipId })
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.first();
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find user by project membership id" });
|
||||
}
|
||||
};
|
||||
|
||||
const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => {
|
||||
try {
|
||||
const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*");
|
||||
@@ -111,6 +122,7 @@ export const userDALFactory = (db: TDbClient) => {
|
||||
findUserEncKeyByEmail,
|
||||
findUserEncKeyByUserId,
|
||||
updateUserEncryptionByUserId,
|
||||
findUserByProjectMembershipId,
|
||||
upsertUserEncryptionKey,
|
||||
createUserEncryption,
|
||||
findOneUserAction,
|
||||
|
||||
Reference in New Issue
Block a user