merge main and deconflict

This commit is contained in:
Scott Wilson
2025-08-06 18:50:02 -07:00
204 changed files with 5291 additions and 3396 deletions
+1
View File
@@ -99,6 +99,7 @@ const main = async () => {
(el) =>
!el.tableName.includes("_migrations") &&
!el.tableName.includes("audit_logs_") &&
!el.tableName.includes("active_locks") &&
el.tableName !== "intermediate_audit_logs"
);
+2
View File
@@ -18,6 +18,7 @@ import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/extern
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
@@ -300,6 +301,7 @@ declare module "fastify" {
reminder: TReminderServiceFactory;
bus: TEventBusService;
sse: TServerSentEventsService;
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
+10
View File
@@ -494,6 +494,11 @@ import {
TAccessApprovalPoliciesEnvironmentsInsert,
TAccessApprovalPoliciesEnvironmentsUpdate
} from "@app/db/schemas/access-approval-policies-environments";
import {
TIdentityAuthTemplates,
TIdentityAuthTemplatesInsert,
TIdentityAuthTemplatesUpdate
} from "@app/db/schemas/identity-auth-templates";
import {
TIdentityLdapAuths,
TIdentityLdapAuthsInsert,
@@ -878,6 +883,11 @@ declare module "knex/types/tables" {
TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate
>;
[TableName.IdentityAuthTemplate]: KnexOriginal.CompositeTableType<
TIdentityAuthTemplates,
TIdentityAuthTemplatesInsert,
TIdentityAuthTemplatesUpdate
>;
[TableName.AccessApprovalPolicy]: KnexOriginal.CompositeTableType<
TAccessApprovalPolicies,
@@ -0,0 +1,18 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.UserEncryptionKey, (table) => {
table.text("encryptedPrivateKey").nullable().alter();
table.text("publicKey").nullable().alter();
table.text("iv").nullable().alter();
table.text("tag").nullable().alter();
table.text("salt").nullable().alter();
table.text("verifier").nullable().alter();
});
}
export async function down(): Promise<void> {
// do nothing for now to avoid breaking down migrations
}
@@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityAuthTemplate))) {
await knex.schema.createTable(TableName.IdentityAuthTemplate, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.binary("templateFields").notNullable();
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.string("name", 64).notNullable();
t.string("authMethod").notNullable();
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.IdentityAuthTemplate);
}
if (!(await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId"))) {
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
t.uuid("templateId").nullable();
t.foreign("templateId").references("id").inTable(TableName.IdentityAuthTemplate).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId")) {
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
t.dropForeign(["templateId"]);
t.dropColumn("templateId");
});
}
await knex.schema.dropTableIfExists(TableName.IdentityAuthTemplate);
await dropOnUpdateTrigger(knex, TableName.IdentityAuthTemplate);
}
@@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const IdentityAuthTemplatesSchema = z.object({
id: z.string().uuid(),
templateFields: zodBuffer,
orgId: z.string().uuid(),
name: z.string(),
authMethod: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityAuthTemplates = z.infer<typeof IdentityAuthTemplatesSchema>;
export type TIdentityAuthTemplatesInsert = Omit<z.input<typeof IdentityAuthTemplatesSchema>, TImmutableDBKeys>;
export type TIdentityAuthTemplatesUpdate = Partial<Omit<z.input<typeof IdentityAuthTemplatesSchema>, TImmutableDBKeys>>;
@@ -25,7 +25,8 @@ export const IdentityLdapAuthsSchema = z.object({
allowedFields: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
accessTokenPeriod: z.coerce.number().default(0)
accessTokenPeriod: z.coerce.number().default(0),
templateId: z.string().uuid().nullable().optional()
});
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;
+1
View File
@@ -91,6 +91,7 @@ export enum TableName {
IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role",
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
IdentityAuthTemplate = "identity_auth_templates",
// used by both identity and users
IdentityMetadata = "identity_metadata",
ResourceMetadata = "resource_metadata",
@@ -15,12 +15,12 @@ export const UserEncryptionKeysSchema = z.object({
protectedKey: z.string().nullable().optional(),
protectedKeyIV: z.string().nullable().optional(),
protectedKeyTag: z.string().nullable().optional(),
publicKey: z.string(),
encryptedPrivateKey: z.string(),
iv: z.string(),
tag: z.string(),
salt: z.string(),
verifier: z.string(),
publicKey: z.string().nullable().optional(),
encryptedPrivateKey: z.string().nullable().optional(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
salt: z.string().nullable().optional(),
verifier: z.string().nullable().optional(),
userId: z.string().uuid(),
hashedPassword: z.string().nullable().optional(),
serverEncryptedPrivateKey: z.string().nullable().optional(),
+4
View File
@@ -115,6 +115,10 @@ export const generateUserSrpKeys = async (password: string) => {
};
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
throw new Error("User encrypted private key not found");
}
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,
+4 -3
View File
@@ -1,7 +1,7 @@
import { Knex } from "knex";
import { crypto } from "@app/lib/crypto";
import { initLogger } from "@app/lib/logger";
import { initEnvConfig } from "@app/lib/config/env";
import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { AuthMethod } from "../../services/auth/auth-type";
@@ -17,7 +17,7 @@ export async function seed(knex: Knex): Promise<void> {
initLogger();
const superAdminDAL = superAdminDALFactory(knex);
await crypto.initialize(superAdminDAL);
await initEnvConfig(superAdminDAL, logger);
await knex(TableName.SuperAdmin).insert([
// eslint-disable-next-line
@@ -25,6 +25,7 @@ export async function seed(knex: Knex): Promise<void> {
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
]);
// Inserts seed entries
const [user] = await knex(TableName.Users)
.insert([
{
+180 -19
View File
@@ -1,9 +1,28 @@
import { Knex } from "knex";
import { initEnvConfig } from "@app/lib/config/env";
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { initLogger, logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AuthMethod } from "@app/services/auth/auth-type";
import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns";
import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { userDALFactory } from "@app/services/user/user-dal";
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
import {
OrgMembershipRole,
OrgMembershipStatus,
ProjectMembershipRole,
ProjectType,
SecretEncryptionAlgo,
SecretKeyEncoding,
TableName
} from "../schemas";
import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
@@ -11,12 +30,159 @@ export const DEFAULT_PROJECT_ENVS = [
{ name: "Production", slug: "prod" }
];
const createUserWithGhostUser = async (
orgId: string,
projectId: string,
userId: string,
userOrgMembershipId: string,
knex: Knex
) => {
const projectKeyDAL = projectKeyDALFactory(knex);
const userDAL = userDALFactory(knex);
const projectMembershipDAL = projectMembershipDALFactory(knex);
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(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 [ghostUser] = await knex(TableName.Users)
.insert({
isGhost: true,
authMethods: [AuthMethod.EMAIL],
username: email,
email,
isAccepted: true
})
.returning("*");
const encKeys = await generateUserSrpKeys(email, password);
await knex(TableName.UserEncryptionKey)
.insert({ userId: ghostUser.id, encryptionVersion: 2, publicKey: encKeys.publicKey })
.onConflict("userId")
.merge();
await knex(TableName.OrgMembership)
.insert({
orgId,
userId: ghostUser.id,
role: OrgMembershipRole.Admin,
status: OrgMembershipStatus.Accepted,
isActive: true
})
.returning("*");
const [projectMembership] = await knex(TableName.ProjectMembership)
.insert({
userId: ghostUser.id,
projectId
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
projectMembershipId: projectMembership.id,
role: ProjectMembershipRole.Admin
});
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: encKeys.publicKey,
privateKey: encKeys.plainPrivateKey
});
await knex(TableName.ProjectKeys).insert({
projectId,
receiverId: ghostUser.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: ghostUser.id
});
const { iv, tag, ciphertext, encoding, algorithm } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(encKeys.plainPrivateKey);
await knex(TableName.ProjectBot).insert({
name: "Infisical Bot (Ghost)",
projectId,
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: encKeys.publicKey,
senderId: ghostUser.id,
algorithm,
keyEncoding: encoding
});
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, knex);
if (!latestKey) {
throw new Error("Latest key not found for user");
}
const user = await userDAL.findUserEncKeyByUserId(userId, knex);
if (!user || !user.publicKey) {
throw new Error("User not found");
}
const [projectAdmin] = assignWorkspaceKeysToMembers({
decryptKey: latestKey,
userPrivateKey: encKeys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: userOrgMembershipId
}
]
});
// Create a membership for the user
const userProjectMembership = await projectMembershipDAL.create(
{
projectId,
userId: user.id
},
knex
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
knex
);
// Create a project key for the user
await projectKeyDAL.create(
{
encryptedKey: projectAdmin.workspaceEncryptedKey,
nonce: projectAdmin.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
},
knex
);
return {
user: ghostUser,
keys: encKeys
};
};
export async function seed(knex: Knex): Promise<void> {
// Deletes ALL existing entries
await knex(TableName.Project).del();
await knex(TableName.Environment).del();
await knex(TableName.SecretFolder).del();
initLogger();
const superAdminDAL = superAdminDALFactory(knex);
await initEnvConfig(superAdminDAL, logger);
const [project] = await knex(TableName.Project)
.insert({
name: seedData1.project.name,
@@ -29,29 +195,24 @@ export async function seed(knex: Knex): Promise<void> {
})
.returning("*");
const projectMembership = await knex(TableName.ProjectMembership)
.insert({
projectId: project.id,
const userOrgMembership = await knex(TableName.OrgMembership)
.where({
orgId: seedData1.organization.id,
userId: seedData1.id
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectMembershipId: projectMembership[0].id
});
.first();
if (!userOrgMembership) {
throw new Error("User org membership not found");
}
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
if (!user) throw new Error("User not found");
const userPrivateKey = await getUserPrivateKey(seedData1.password, user);
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey);
await knex(TableName.ProjectKeys).insert({
projectId: project.id,
nonce: projectKey.nonce,
encryptedKey: projectKey.ciphertext,
receiverId: seedData1.id,
senderId: seedData1.id
});
if (!user.publicKey) {
throw new Error("User public key not found");
}
await createUserWithGhostUser(seedData1.organization.id, project.id, seedData1.id, userOrgMembership.id, knex);
// create default environments and default folders
const envs = await knex(TableName.Environment)
@@ -1,6 +1,9 @@
import { Knex } from "knex";
import { initEnvConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
import { seedData1 } from "../seed-data";
@@ -10,6 +13,11 @@ export async function seed(knex: Knex): Promise<void> {
await knex(TableName.Identity).del();
await knex(TableName.IdentityOrgMembership).del();
initLogger();
const superAdminDAL = superAdminDALFactory(knex);
await initEnvConfig(superAdminDAL, logger);
// Inserts seed entries
await knex(TableName.Identity).insert([
{
@@ -0,0 +1,391 @@
import { z } from "zod";
import { IdentityAuthTemplatesSchema } from "@app/db/schemas/identity-auth-templates";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
IdentityAuthTemplateMethod,
TEMPLATE_SUCCESS_MESSAGES,
TEMPLATE_VALIDATION_MESSAGES
} from "@app/ee/services/identity-auth-template/identity-auth-template-enums";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const ldapTemplateFieldsSchema = z.object({
url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.URL_REQUIRED),
bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_DN_REQUIRED),
bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_PASSWORD_REQUIRED),
searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.SEARCH_BASE_REQUIRED),
ldapCaCertificate: z.string().trim().optional()
});
export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Create identity auth template",
security: [
{
bearerAuth: []
}
],
body: z.object({
name: z
.string()
.trim()
.min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED)
.max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH),
authMethod: z.nativeEnum(IdentityAuthTemplateMethod),
templateFields: ldapTemplateFieldsSchema
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: z.record(z.string(), z.unknown())
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.createTemplate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.body.name,
authMethod: req.body.authMethod,
templateFields: req.body.templateFields
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE,
metadata: {
templateId: template.id,
name: template.name
}
}
});
return template;
}
});
server.route({
method: "PATCH",
url: "/:templateId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Update identity auth template",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
}),
body: z.object({
name: z
.string()
.trim()
.min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED)
.max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH)
.optional(),
templateFields: ldapTemplateFieldsSchema.partial().optional()
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: z.record(z.string(), z.unknown())
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.updateTemplate({
templateId: req.params.templateId,
name: req.body.name,
templateFields: req.body.templateFields,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE,
metadata: {
templateId: template.id,
name: template.name
}
}
});
return template;
}
});
server.route({
method: "DELETE",
url: "/:templateId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Delete identity auth template",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.deleteTemplate({
templateId: req.params.templateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE,
metadata: {
templateId: template.id,
name: template.name
}
}
});
return { message: TEMPLATE_SUCCESS_MESSAGES.DELETED };
}
});
server.route({
method: "GET",
url: "/:templateId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Get identity auth template by ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: ldapTemplateFieldsSchema
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.getTemplate({
templateId: req.params.templateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return template;
}
});
server.route({
method: "GET",
url: "/search",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "List identity auth templates",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
limit: z.coerce.number().positive().max(100).default(5).optional(),
offset: z.coerce.number().min(0).default(0).optional(),
search: z.string().optional()
}),
response: {
200: z.object({
templates: IdentityAuthTemplatesSchema.extend({
templateFields: ldapTemplateFieldsSchema
}).array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { templates, totalCount } = await server.services.identityAuthTemplate.listTemplates({
limit: req.query.limit,
offset: req.query.offset,
search: req.query.search,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { templates, totalCount };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Get identity auth templates by authentication method",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
authMethod: z.nativeEnum(IdentityAuthTemplateMethod)
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: ldapTemplateFieldsSchema
}).array()
}
},
handler: async (req) => {
const templates = await server.services.identityAuthTemplate.getTemplatesByAuthMethod({
authMethod: req.query.authMethod,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return templates;
}
});
server.route({
method: "GET",
url: "/:templateId/usage",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Get template usage by template ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string()
}),
response: {
200: z
.object({
identityId: z.string(),
identityName: z.string()
})
.array()
}
},
handler: async (req) => {
const templates = await server.services.identityAuthTemplate.findTemplateUsages({
templateId: req.params.templateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return templates;
}
});
server.route({
method: "POST",
url: "/:templateId/delete-usage",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Unlink identity auth template usage",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string()
}),
body: z.object({
identityIds: z.string().array()
}),
response: {
200: z
.object({
authId: z.string(),
identityId: z.string(),
identityName: z.string()
})
.array()
}
},
handler: async (req) => {
const templates = await server.services.identityAuthTemplate.unlinkTemplateUsage({
templateId: req.params.templateId,
identityIds: req.body.identityIds,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return templates;
}
});
};
+2
View File
@@ -13,6 +13,7 @@ import { registerGatewayRouter } from "./gateway-router";
import { registerGithubOrgSyncRouter } from "./github-org-sync-router";
import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerIdentityTemplateRouter } from "./identity-template-router";
import { registerKmipRouter } from "./kmip-router";
import { registerKmipSpecRouter } from "./kmip-spec-router";
import { registerLdapRouter } from "./ldap-router";
@@ -125,6 +126,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerExternalKmsRouter, {
prefix: "/external-kms"
});
await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
@@ -161,6 +161,9 @@ export enum EventType {
CREATE_IDENTITY = "create-identity",
UPDATE_IDENTITY = "update-identity",
DELETE_IDENTITY = "delete-identity",
MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE = "machine-identity-auth-template-create",
MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE = "machine-identity-auth-template-update",
MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE = "machine-identity-auth-template-delete",
LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth",
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
@@ -830,6 +833,30 @@ interface LoginIdentityUniversalAuthEvent {
};
}
interface MachineIdentityAuthTemplateCreateEvent {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE;
metadata: {
templateId: string;
name: string;
};
}
interface MachineIdentityAuthTemplateUpdateEvent {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE;
metadata: {
templateId: string;
name: string;
};
}
interface MachineIdentityAuthTemplateDeleteEvent {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE;
metadata: {
templateId: string;
name: string;
};
}
interface AddIdentityUniversalAuthEvent {
type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH;
metadata: {
@@ -1325,6 +1352,7 @@ interface AddIdentityLdapAuthEvent {
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
allowedFields?: TAllowedFields[];
url: string;
templateId?: string | null;
};
}
@@ -1338,6 +1366,7 @@ interface UpdateIdentityLdapAuthEvent {
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
allowedFields?: TAllowedFields[];
url?: string;
templateId?: string | null;
};
}
@@ -3439,6 +3468,9 @@ export type Event =
| UpdateIdentityEvent
| DeleteIdentityEvent
| LoginIdentityUniversalAuthEvent
| MachineIdentityAuthTemplateCreateEvent
| MachineIdentityAuthTemplateUpdateEvent
| MachineIdentityAuthTemplateDeleteEvent
| AddIdentityUniversalAuthEvent
| UpdateIdentityUniversalAuthEvent
| DeleteIdentityUniversalAuthEvent
@@ -13,11 +13,9 @@ const AUTH_REFRESH_INTERVAL = 60 * 1000;
const HEART_BEAT_INTERVAL = 15 * 1000;
export const sseServiceFactory = (bus: TEventBusService, redis: Redis) => {
let heartbeatInterval: NodeJS.Timeout | null = null;
const clients = new Set<EventStreamClient>();
heartbeatInterval = setInterval(() => {
const heartbeatInterval = setInterval(() => {
for (const client of clients) {
if (client.stream.closed) continue;
void client.ping();
@@ -66,15 +66,24 @@ export type EventStreamClient = {
};
export function createEventStreamClient(redis: Redis, options: IEventStreamClientOpts): EventStreamClient {
const rules = options.registered.map((r) => ({
subject: options.type,
action: "subscribe",
conditions: {
eventType: r.event,
secretPath: r.conditions?.secretPath ?? "/",
environment: r.conditions?.environmentSlug
}
}));
const rules = options.registered.map((r) => {
const secretPath = r.conditions?.secretPath;
const hasConditions = r.conditions?.environmentSlug || r.conditions?.secretPath;
return {
subject: options.type,
action: "subscribe",
conditions: {
eventType: r.event,
...(hasConditions
? {
environment: r.conditions?.environmentSlug ?? "",
secretPath: { $glob: secretPath }
}
: {})
}
};
});
const id = `sse-${nanoid()}`;
const control = new AbortController();
+25 -1
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex";
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
@@ -65,6 +65,18 @@ const addAcceptedUsersToGroup = async ({
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
for await (const projectId of projectIds) {
const project = await projectDAL.findById(projectId, tx);
if (!project) {
throw new NotFoundError({
message: `Failed to find project with ID '${projectId}'`
});
}
if (project.version !== ProjectVersion.V1 && project.version !== ProjectVersion.V2) {
// eslint-disable-next-line no-continue
continue;
}
const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
if (usersToAddProjectKeyFor.length) {
@@ -86,6 +98,12 @@ const addAcceptedUsersToGroup = async ({
});
}
if (!ghostUserLatestKey.sender.publicKey) {
throw new NotFoundError({
message: `Failed to find project owner's public key in project with ID '${projectId}'`
});
}
const bot = await projectBotDAL.findOne({ projectId }, tx);
if (!bot) {
@@ -112,6 +130,12 @@ const addAcceptedUsersToGroup = async ({
});
const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
if (!user.publicKey) {
throw new NotFoundError({
message: `Failed to find user's public key in project with ID '${projectId}'`
});
}
const { ciphertext: encryptedKey, nonce } = crypto
.encryption()
.asymmetric()
@@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = {
TUserGroupMembershipDALFactory,
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
>;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
+3 -3
View File
@@ -65,7 +65,7 @@ export type TAddUsersToGroup = {
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx: Knex;
};
@@ -78,7 +78,7 @@ export type TAddUsersToGroupByUserIds = {
orgDAL: Pick<TOrgDALFactory, "findMembership">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};
@@ -102,7 +102,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};
@@ -0,0 +1,83 @@
/* eslint-disable no-case-declarations */
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { buildFindFilter, ormify } from "@app/lib/knex";
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
export type TIdentityAuthTemplateDALFactory = ReturnType<typeof identityAuthTemplateDALFactory>;
export const identityAuthTemplateDALFactory = (db: TDbClient) => {
const identityAuthTemplateOrm = ormify(db, TableName.IdentityAuthTemplate);
const findByOrgId = async (
orgId: string,
{ limit, offset, search, tx }: { limit?: number; offset?: number; search?: string; tx?: Knex } = {}
) => {
let query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId });
let countQuery = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId });
if (search) {
const searchFilter = `%${search.toLowerCase()}%`;
query = query.whereRaw("LOWER(name) LIKE ?", [searchFilter]);
countQuery = countQuery.whereRaw("LOWER(name) LIKE ?", [searchFilter]);
}
query = query.orderBy("createdAt", "desc");
if (limit !== undefined) {
query = query.limit(limit);
}
if (offset !== undefined) {
query = query.offset(offset);
}
const docs = await query;
const [{ count }] = (await countQuery.count("* as count")) as [{ count: string | number }];
return { docs, totalCount: Number(count) };
};
const findByAuthMethod = async (authMethod: string, orgId: string, tx?: Knex) => {
const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate)
.where({ authMethod, orgId })
.orderBy("createdAt", "desc");
const docs = await query;
return docs;
};
const findTemplateUsages = async (templateId: string, authMethod: string, tx?: Knex) => {
switch (authMethod) {
case IdentityAuthTemplateMethod.LDAP:
const query = (tx || db.replicaNode())(TableName.IdentityLdapAuth)
.join(TableName.Identity, `${TableName.IdentityLdapAuth}.identityId`, `${TableName.Identity}.id`)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ templateId }, TableName.IdentityLdapAuth))
.select(
db.ref("identityId").withSchema(TableName.IdentityLdapAuth),
db.ref("name").withSchema(TableName.Identity).as("identityName")
);
const docs = await query;
return docs;
default:
return [];
}
};
const findByIdAndOrgId = async (id: string, orgId: string, tx?: Knex) => {
const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ id, orgId });
const doc = await query;
return doc?.[0];
};
return {
...identityAuthTemplateOrm,
findByOrgId,
findByAuthMethod,
findTemplateUsages,
findByIdAndOrgId
};
};
@@ -0,0 +1,22 @@
export enum IdentityAuthTemplateMethod {
LDAP = "ldap"
}
export const TEMPLATE_VALIDATION_MESSAGES = {
TEMPLATE_NAME_REQUIRED: "Template name is required",
TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long",
AUTH_METHOD_REQUIRED: "Auth method is required",
TEMPLATE_ID_REQUIRED: "Template ID is required",
LDAP: {
URL_REQUIRED: "LDAP URL is required",
BIND_DN_REQUIRED: "Bind DN is required",
BIND_PASSWORD_REQUIRED: "Bind password is required",
SEARCH_BASE_REQUIRED: "Search base is required"
}
} as const;
export const TEMPLATE_SUCCESS_MESSAGES = {
CREATED: "Template created successfully",
UPDATED: "Template updated successfully",
DELETED: "Template deleted successfully"
} as const;
@@ -0,0 +1,454 @@
import { ForbiddenError } from "@casl/ability";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal";
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
import {
TDeleteIdentityAuthTemplateDTO,
TFindTemplateUsagesDTO,
TGetIdentityAuthTemplateDTO,
TGetTemplatesByAuthMethodDTO,
TLdapTemplateFields,
TListIdentityAuthTemplatesDTO,
TUnlinkTemplateUsageDTO
} from "./identity-auth-template-types";
type TIdentityAuthTemplateServiceFactoryDep = {
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
identityLdapAuthDAL: TIdentityLdapAuthDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export type TIdentityAuthTemplateServiceFactory = ReturnType<typeof identityAuthTemplateServiceFactory>;
export const identityAuthTemplateServiceFactory = ({
identityAuthTemplateDAL,
identityLdapAuthDAL,
permissionService,
kmsService,
licenseService,
auditLogService
}: TIdentityAuthTemplateServiceFactoryDep) => {
// Plan check
const $checkPlan = async (orgId: string) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.machineIdentityAuthTemplates)
throw new BadRequestError({
message:
"Failed to use identity auth template due to plan restriction. Upgrade plan to access machine identity auth templates."
});
};
const createTemplate = async ({
name,
authMethod,
templateFields,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: {
name: string;
authMethod: string;
templateFields: Record<string, unknown>;
} & Omit<TOrgPermission, "orgId">) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const template = await identityAuthTemplateDAL.create({
name,
authMethod,
templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob,
orgId: actorOrgId
});
return { ...template, templateFields };
};
const updateTemplate = async ({
templateId,
name,
templateFields,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: {
templateId: string;
name?: string;
templateFields?: Record<string, unknown>;
} & Omit<TOrgPermission, "orgId">) => {
await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
template.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: template.orgId
});
let finalTemplateFields: Record<string, unknown> = {};
const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
const authTemplate = await identityAuthTemplateDAL.updateById(
templateId,
{
name,
...(templateFields && {
templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob
})
},
tx
);
if (templateFields && template.authMethod === IdentityAuthTemplateMethod.LDAP) {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: template.orgId
});
const currentTemplateFields = JSON.parse(
decryptor({ cipherTextBlob: template.templateFields }).toString()
) as TLdapTemplateFields;
const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields };
finalTemplateFields = mergedTemplateFields;
const ldapUpdateData: {
url?: string;
searchBase?: string;
encryptedBindDN?: Buffer;
encryptedBindPass?: Buffer;
encryptedLdapCaCertificate?: Buffer;
} = {};
if ("url" in templateFields) {
ldapUpdateData.url = mergedTemplateFields.url;
}
if ("searchBase" in templateFields) {
ldapUpdateData.searchBase = mergedTemplateFields.searchBase;
}
if ("bindDN" in templateFields) {
ldapUpdateData.encryptedBindDN = encryptor({
plainText: Buffer.from(mergedTemplateFields.bindDN)
}).cipherTextBlob;
}
if ("bindPass" in templateFields) {
ldapUpdateData.encryptedBindPass = encryptor({
plainText: Buffer.from(mergedTemplateFields.bindPass)
}).cipherTextBlob;
}
if ("ldapCaCertificate" in templateFields) {
ldapUpdateData.encryptedLdapCaCertificate = encryptor({
plainText: Buffer.from(mergedTemplateFields.ldapCaCertificate || "")
}).cipherTextBlob;
}
if (Object.keys(ldapUpdateData).length > 0) {
const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, ldapUpdateData, tx);
await Promise.all(
updatedLdapAuths.map(async (updatedLdapAuth) => {
await auditLogService.createAuditLog({
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
orgId: actorOrgId,
event: {
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
metadata: {
identityId: updatedLdapAuth.identityId,
templateId: template.id
}
}
});
})
);
}
}
return authTemplate;
});
return { ...updatedTemplate, templateFields: finalTemplateFields };
};
const deleteTemplate = async ({
templateId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TDeleteIdentityAuthTemplateDTO) => {
await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
template.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
// Remove template reference from identityLdapAuth records
const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, { templateId: null }, tx);
await Promise.all(
updatedLdapAuths.map(async (updatedLdapAuth) => {
await auditLogService.createAuditLog({
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
orgId: actorOrgId,
event: {
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
metadata: {
identityId: updatedLdapAuth.identityId,
templateId: template.id
}
}
});
})
);
// Delete the template
const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx);
return deletedTpl;
});
return deletedTemplate;
};
const getTemplate = async ({
templateId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetIdentityAuthTemplateDTO) => {
await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
template.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: template.orgId
});
const decryptedTemplateFields = decryptor({ cipherTextBlob: template.templateFields }).toString();
return {
...template,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
templateFields: JSON.parse(decryptedTemplateFields)
};
};
const listTemplates = async ({
limit,
offset,
search,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TListIdentityAuthTemplatesDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { docs, totalCount } = await identityAuthTemplateDAL.findByOrgId(actorOrgId, { limit, offset, search });
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
return {
totalCount,
templates: docs.map((doc) => ({
...doc,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString())
}))
};
};
const getTemplatesByAuthMethod = async ({
authMethod,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetTemplatesByAuthMethodDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const docs = await identityAuthTemplateDAL.findByAuthMethod(authMethod, actorOrgId);
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
return docs.map((doc) => ({
...doc,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString())
}));
};
const findTemplateUsages = async ({
templateId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TFindTemplateUsagesDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const docs = await identityAuthTemplateDAL.findTemplateUsages(templateId, template.authMethod);
return docs;
};
const unlinkTemplateUsage = async ({
templateId,
identityIds,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUnlinkTemplateUsageDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
switch (template.authMethod) {
case IdentityAuthTemplateMethod.LDAP:
await identityLdapAuthDAL.update({ $in: { identityId: identityIds }, templateId }, { templateId: null });
break;
default:
break;
}
};
return {
createTemplate,
updateTemplate,
deleteTemplate,
getTemplate,
listTemplates,
getTemplatesByAuthMethod,
findTemplateUsages,
unlinkTemplateUsage
};
};
@@ -0,0 +1,61 @@
import { TProjectPermission } from "@app/lib/types";
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
// Method-specific template field types
export type TLdapTemplateFields = {
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
ldapCaCertificate?: string;
};
// Union type for all template field types
export type TTemplateFieldsByMethod = {
[IdentityAuthTemplateMethod.LDAP]: TLdapTemplateFields;
};
// Generic base types that use conditional types for type safety
export type TCreateIdentityAuthTemplateDTO = {
name: string;
authMethod: IdentityAuthTemplateMethod;
templateFields: TTemplateFieldsByMethod[IdentityAuthTemplateMethod];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateIdentityAuthTemplateDTO = {
templateId: string;
name?: string;
templateFields?: Partial<TTemplateFieldsByMethod[IdentityAuthTemplateMethod]>;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteIdentityAuthTemplateDTO = {
templateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetIdentityAuthTemplateDTO = {
templateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TListIdentityAuthTemplatesDTO = {
limit?: number;
offset?: number;
search?: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetTemplatesByAuthMethodDTO = {
authMethod: string;
} & Omit<TProjectPermission, "projectId">;
export type TFindTemplateUsagesDTO = {
templateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TUnlinkTemplateUsageDTO = {
templateId: string;
identityIds: string[];
} & Omit<TProjectPermission, "projectId">;
// Specific LDAP types for convenience
export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO;
export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO;
@@ -0,0 +1,6 @@
export type { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal";
export { identityAuthTemplateDALFactory } from "./identity-auth-template-dal";
export * from "./identity-auth-template-enums";
export type { TIdentityAuthTemplateServiceFactory } from "./identity-auth-template-service";
export { identityAuthTemplateServiceFactory } from "./identity-auth-template-service";
export type * from "./identity-auth-template-types";
@@ -55,7 +55,7 @@ type TLdapConfigServiceFactoryDep = {
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
@@ -31,7 +31,8 @@ export const getDefaultOnPremFeatures = () => {
caCrl: false,
sshHostGroups: false,
enterpriseSecretSyncs: false,
enterpriseAppConnections: false
enterpriseAppConnections: false,
machineIdentityAuthTemplates: false
};
};
@@ -60,7 +60,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
enterpriseSecretSyncs: false,
enterpriseAppConnections: false,
fips: false,
eventSubscriptions: false
eventSubscriptions: false,
machineIdentityAuthTemplates: false
});
export const setupLicenseRequestWithStore = (
@@ -75,6 +75,7 @@ export type TFeatureSet = {
secretScanning: false;
enterpriseSecretSyncs: false;
enterpriseAppConnections: false;
machineIdentityAuthTemplates: false;
fips: false;
eventSubscriptions: false;
};
@@ -79,7 +79,7 @@ type TOidcConfigServiceFactoryDep = {
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -28,6 +28,15 @@ export enum OrgPermissionKmipActions {
Setup = "setup"
}
export enum OrgPermissionMachineIdentityAuthTemplateActions {
ListTemplates = "list-templates",
EditTemplates = "edit-templates",
CreateTemplates = "create-templates",
DeleteTemplates = "delete-templates",
UnlinkTemplates = "unlink-templates",
AttachTemplates = "attach-templates"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
@@ -88,6 +97,7 @@ export enum OrgPermissionSubjects {
Identity = "identity",
Kms = "kms",
AdminConsole = "organization-admin-console",
MachineIdentityAuthTemplate = "machine-identity-auth-template",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections",
@@ -126,6 +136,7 @@ export type OrgPermissionSet =
)
]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionMachineIdentityAuthTemplateActions, OrgPermissionSubjects.MachineIdentityAuthTemplate]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
@@ -237,6 +248,14 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
"Describe what action an entity can take."
)
}),
z.object({
subject: z
.literal(OrgPermissionSubjects.MachineIdentityAuthTemplate)
.describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionMachineIdentityAuthTemplateActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
@@ -350,6 +369,25 @@ const buildAdminPermission = () => {
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare);
return rules;
@@ -385,6 +423,16 @@ const buildMemberPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
return rules;
};
+1 -1
View File
@@ -59,7 +59,7 @@ type TScimServiceFactoryDep = {
TOrgMembershipDALFactory,
"find" | "findOne" | "create" | "updateById" | "findById" | "update"
>;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser" | "findById">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick<
TGroupDALFactory,
+13 -2
View File
@@ -18,6 +18,7 @@ import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/
export enum ApiDocsTags {
Identities = "Identities",
IdentityTemplates = "Identity Templates",
TokenAuth = "Token Auth",
UniversalAuth = "Universal Auth",
GcpAuth = "GCP Auth",
@@ -69,7 +70,8 @@ export enum ApiDocsTags {
SecretScanning = "Secret Scanning",
OidcSso = "OIDC SSO",
SamlSso = "SAML SSO",
LdapSso = "LDAP SSO"
LdapSso = "LDAP SSO",
Events = "Event Subscriptions"
}
export const GROUPS = {
@@ -214,6 +216,7 @@ export const LDAP_AUTH = {
password: "The password of the LDAP user to login."
},
ATTACH: {
templateId: "The ID of the identity auth template to attach the configuration onto.",
identityId: "The ID of the identity to attach the configuration onto.",
url: "The URL of the LDAP server.",
allowedFields:
@@ -240,7 +243,8 @@ export const LDAP_AUTH = {
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
templateId: "The ID of the identity auth template to update the configuration to."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the configuration for."
@@ -2869,3 +2873,10 @@ export const LdapSso = {
caCert: "The CA certificate to use when verifying the LDAP server certificate."
}
};
export const EventSubscriptions = {
SUBSCRIBE_PROJECT_EVENTS: {
projectId: "The ID of the project to subscribe to events for.",
register: "List of events you want to subscribe to"
}
};
+17 -1
View File
@@ -53,7 +53,7 @@ type DecryptedIntegrationAuths = z.infer<typeof DecryptedIntegrationAuthsSchema>
type TLatestKey = TProjectKeys & {
sender: {
publicKey: string;
publicKey?: string;
};
};
@@ -91,6 +91,10 @@ const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: s
return results;
};
export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
@@ -143,6 +147,10 @@ export const decryptSecretVersions = (
privateKey: string,
latestKey: TLatestKey
) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
@@ -195,6 +203,10 @@ export const decryptSecretApprovals = (
privateKey: string,
latestKey: TLatestKey
) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
@@ -247,6 +259,10 @@ export const decryptIntegrationAuths = (
privateKey: string,
latestKey: TLatestKey
) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
+18
View File
@@ -4,6 +4,7 @@ import jsrp from "jsrp";
import { TUserEncryptionKeys } from "@app/db/schemas";
import { UserEncryption } from "@app/services/user/user-types";
import { BadRequestError } from "../errors";
import { crypto, SymmetricKeySize } from "./cryptography";
export const generateSrpServerKey = async (salt: string, verifier: string) => {
@@ -127,6 +128,10 @@ export const getUserPrivateKey = async (
>
) => {
if (user.encryptionVersion === UserEncryption.V1) {
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
throw new BadRequestError({ message: "User encrypted private key not found" });
}
return crypto
.encryption()
.symmetric()
@@ -138,12 +143,25 @@ export const getUserPrivateKey = async (
keySize: SymmetricKeySize.Bits128
});
}
// still used for legacy things
if (
user.encryptionVersion === UserEncryption.V2 &&
user.protectedKey &&
user.protectedKeyIV &&
user.protectedKeyTag
) {
if (
!user.salt ||
!user.protectedKey ||
!user.protectedKeyIV ||
!user.protectedKeyTag ||
!user.encryptedPrivateKey ||
!user.iv ||
!user.tag
) {
throw new BadRequestError({ message: "User encrypted private key not found" });
}
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,
+15 -6
View File
@@ -179,6 +179,8 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { identityAliCloudAuthDALFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-dal";
import { identityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service";
import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal";
import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service";
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal";
@@ -394,6 +396,7 @@ export const registerRoutes = async (
const identityProjectDAL = identityProjectDALFactory(db);
const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db);
const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
const identityAuthTemplateDAL = identityAuthTemplateDALFactory(db);
const identityTokenAuthDAL = identityTokenAuthDALFactory(db);
const identityUaDAL = identityUaDALFactory(db);
@@ -772,7 +775,6 @@ export const registerRoutes = async (
orgRoleDAL,
permissionService,
orgDAL,
projectBotDAL,
incidentContactDAL,
tokenService,
projectUserAdditionalPrivilegeDAL,
@@ -847,7 +849,6 @@ export const registerRoutes = async (
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
userDAL,
projectBotDAL,
projectKeyDAL,
projectMembershipDAL
@@ -1135,11 +1136,9 @@ export const registerRoutes = async (
projectBotService,
identityProjectDAL,
identityOrgMembershipDAL,
projectKeyDAL,
userDAL,
projectEnvDAL,
orgDAL,
orgService,
projectMembershipDAL,
projectRoleDAL,
folderDAL,
@@ -1159,7 +1158,6 @@ export const registerRoutes = async (
identityProjectMembershipRoleDAL,
keyStore,
kmsService,
projectBotDAL,
certificateTemplateDAL,
projectSlackConfigDAL,
slackIntegrationDAL,
@@ -1461,6 +1459,15 @@ export const registerRoutes = async (
identityMetadataDAL
});
const identityAuthTemplateService = identityAuthTemplateServiceFactory({
identityAuthTemplateDAL,
identityLdapAuthDAL,
permissionService,
kmsService,
licenseService,
auditLogService
});
const identityAccessTokenService = identityAccessTokenServiceFactory({
identityAccessTokenDAL,
identityOrgMembershipDAL,
@@ -1604,7 +1611,8 @@ export const registerRoutes = async (
identityAccessTokenDAL,
identityOrgMembershipDAL,
licenseService,
identityDAL
identityDAL,
identityAuthTemplateDAL
});
const dynamicSecretProviders = buildDynamicSecretProviders({
@@ -2008,6 +2016,7 @@ export const registerRoutes = async (
webhook: webhookService,
serviceToken: serviceTokenService,
identity: identityService,
identityAuthTemplate: identityAuthTemplateService,
identityAccessToken: identityAccessTokenService,
identityProject: identityProjectService,
identityTokenAuth: identityTokenAuthService,
+18 -11
View File
@@ -7,6 +7,7 @@ import { ActionProjectType, ProjectType } from "@app/db/schemas";
import { getServerSentEventsHeaders } from "@app/ee/services/event/event-sse-stream";
import { EventRegisterSchema } from "@app/ee/services/event/types";
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { ApiDocsTags, EventSubscriptions } from "@app/lib/api-docs";
import { BadRequestError, ForbiddenRequestError, RateLimitError } from "@app/lib/errors";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -20,10 +21,14 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.Events],
description: "Subscribe to project events",
body: z.object({
projectId: z.string().trim(),
register: z.array(EventRegisterSchema).max(10)
})
projectId: z.string().trim().describe(EventSubscriptions.SUBSCRIBE_PROJECT_EVENTS.projectId),
register: z.array(EventRegisterSchema).min(1).max(10)
}),
produces: ["text/event-stream"]
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, reply) => {
@@ -75,13 +80,15 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
}
req.body.register.forEach((r) => {
const fields = {
environment: r.conditions?.environmentSlug ?? "",
secretPath: r.conditions?.secretPath ?? "/",
eventType: r.event
};
const allowed = info.permission.can(
ProjectPermissionSecretActions.Subscribe,
subject(ProjectPermissionSub.Secrets, {
environment: r.conditions?.environmentSlug ?? "",
secretPath: r.conditions?.secretPath ?? "/",
eventType: r.event
})
subject(ProjectPermissionSub.Secrets, fields)
);
if (!allowed) {
@@ -89,9 +96,9 @@ export const registerEventRouter = async (server: FastifyZodProvider) => {
name: "PermissionDenied",
message: `You are not allowed to subscribe on secrets`,
details: {
event: r.event,
environmentSlug: r.conditions?.environmentSlug,
secretPath: r.conditions?.secretPath ?? "/"
event: fields.eventType,
environmentSlug: fields.environment,
secretPath: fields.secretPath
}
});
}
@@ -200,49 +200,104 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
params: z.object({
identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId)
}),
body: z
.object({
url: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.url),
bindDN: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindDN),
bindPass: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindPass),
searchBase: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.searchBase),
searchFilter: z
.string()
.trim()
.min(1)
.default("(uid={{username}})")
.refine(isValidLdapFilter, "Invalid LDAP search filter")
.describe(LDAP_AUTH.ATTACH.searchFilter),
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
body: z.union([
// Template-based configuration
z
.object({
templateId: z.string().trim().describe(LDAP_AUTH.ATTACH.templateId),
searchFilter: z
.string()
.trim()
.min(1)
.default("(uid={{username}})")
.refine(isValidLdapFilter, "Invalid LDAP search filter")
.describe(LDAP_AUTH.ATTACH.searchFilter),
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
// Manual configuration
z
.object({
url: z.string().trim().describe(LDAP_AUTH.ATTACH.url),
bindDN: z.string().trim().describe(LDAP_AUTH.ATTACH.bindDN),
bindPass: z.string().trim().describe(LDAP_AUTH.ATTACH.bindPass),
searchBase: z.string().trim().describe(LDAP_AUTH.ATTACH.searchBase),
searchFilter: z
.string()
.trim()
.min(1)
.default("(uid={{username}})")
.refine(isValidLdapFilter, "Invalid LDAP search filter")
.describe(LDAP_AUTH.ATTACH.searchFilter),
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
)
]),
response: {
200: z.object({
identityLdapAuth: IdentityLdapAuthsSchema.omit({
@@ -275,7 +330,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
allowedFields: req.body.allowedFields
allowedFields: req.body.allowedFields,
templateId: identityLdapAuth.templateId
}
}
});
@@ -309,6 +365,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
bindDN: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindDN),
bindPass: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindPass),
searchBase: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.searchBase),
templateId: z.string().trim().optional().describe(LDAP_AUTH.UPDATE.templateId),
searchFilter: z
.string()
.trim()
@@ -376,7 +433,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
allowedFields: req.body.allowedFields
allowedFields: req.body.allowedFields,
templateId: identityLdapAuth.templateId
}
}
});
@@ -413,7 +471,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
}).extend({
bindDN: z.string(),
bindPass: z.string(),
ldapCaCertificate: z.string().optional()
ldapCaCertificate: z.string().optional(),
templateId: z.string().optional().nullable()
})
})
}
@@ -247,7 +247,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
lastName: true,
id: true,
superAdmin: true
}).merge(z.object({ publicKey: z.string().nullable() }))
}).merge(z.object({ publicKey: z.string().nullable().optional() }))
})
)
.omit({ createdAt: true, updatedAt: true })
@@ -9,73 +9,6 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { UserEncryption } from "@app/services/user/user-types";
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/srp1",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
clientPublicKey: z.string().trim()
}),
response: {
200: z.object({
serverPublicKey: z.string(),
salt: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { salt, serverPublicKey } = await server.services.password.generateServerPubKey(
req.permission.id,
req.body.clientPublicKey
);
return { salt, serverPublicKey };
}
});
server.route({
method: "POST",
url: "/change-password",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
clientProof: z.string().trim(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
password: z.string().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req, res) => {
const appCfg = getConfig();
await server.services.password.changePassword({ ...req.body, userId: req.permission.id });
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return { message: "Successfully changed password" };
}
});
server.route({
method: "POST",
url: "/email/password-reset",
@@ -131,41 +64,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "POST",
url: "/backup-private-key",
config: {
rateLimit: authRateLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
clientProof: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
iv: z.string().trim(),
tag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
backupPrivateKey: BackupPrivateKeySchema.omit({ verifier: true })
})
}
},
handler: async (req) => {
const token = validateSignUpAuthorization(req.headers.authorization as string, "", false)!;
const backupPrivateKey = await server.services.password.createBackupPrivateKey({
...req.body,
userId: token.userId
});
if (!backupPrivateKey) throw new Error("Failed to create backup key");
return { message: "Successfully updated backup private key", backupPrivateKey };
}
});
server.route({
method: "GET",
url: "/backup-private-key",
@@ -257,14 +155,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
},
schema: {
body: z.object({
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
password: z.string().trim(),
token: z.string().trim()
}),
@@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.object({
publicKeys: z
.object({
publicKey: z.string().optional(),
publicKey: z.string().nullable().optional(),
userId: z.string()
})
.array()
+4 -38
View File
@@ -1,6 +1,6 @@
import { z } from "zod";
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -19,23 +19,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
schema: {
response: {
200: z.object({
user: UsersSchema.merge(
UserEncryptionKeysSchema.pick({
clientPublicKey: true,
serverPrivateKey: true,
encryptionVersion: true,
protectedKey: true,
protectedKeyIV: true,
protectedKeyTag: true,
publicKey: true,
encryptedPrivateKey: true,
iv: true,
tag: true,
salt: true,
verifier: true,
userId: true
})
)
user: UsersSchema.extend({
encryptionVersion: z.number()
})
})
}
},
@@ -94,26 +80,6 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/private-key",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
const privateKey = await server.services.user.getUserPrivateKey(req.permission.id);
return { privateKey };
}
});
server.route({
method: "GET",
url: "/:userId/unlock",
+7 -7
View File
@@ -97,13 +97,13 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
encryptionVersion: z.number().default(1).nullable().optional(),
protectedKey: z.string().nullable(),
protectedKeyIV: z.string().nullable(),
protectedKeyTag: z.string().nullable(),
publicKey: z.string(),
encryptedPrivateKey: z.string(),
iv: z.string(),
tag: z.string(),
protectedKey: z.string().nullish(),
protectedKeyIV: z.string().nullish(),
protectedKeyTag: z.string().nullish(),
publicKey: z.string().nullish(),
encryptedPrivateKey: z.string().nullish(),
iv: z.string().nullish(),
tag: z.string().nullish(),
token: z.string()
})
}
@@ -153,7 +153,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
firstName: true,
lastName: true,
id: true
}).extend({ publicKey: z.string().nullable() })
}).extend({ publicKey: z.string().nullish() })
}).omit({ createdAt: true, updatedAt: true })
})
}
@@ -1,5 +1,6 @@
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
@@ -41,13 +42,38 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
rateLimit: authRateLimit
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
handler: async (req, res) => {
const appCfg = getConfig();
await server.services.password.resetPasswordV2({
type: ResetPasswordV2Type.LoggedInReset,
userId: req.permission.id,
newPassword: req.body.newPassword,
oldPassword: req.body.oldPassword
});
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
void res.cookie("aod", "", {
httpOnly: false,
path: "/",
sameSite: "lax",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
}
});
};
@@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: ProjectKeysSchema.merge(
z.object({
sender: z.object({
publicKey: z.string()
publicKey: z.string().optional()
})
})
)
+65 -10
View File
@@ -20,8 +20,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
serverPublicKey: z.string(),
salt: z.string()
serverPublicKey: z.string().nullish(),
salt: z.string().nullish()
})
}
},
@@ -124,14 +124,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
encryptionVersion: z.number().default(1).nullable().optional(),
protectedKey: z.string().nullable(),
protectedKeyIV: z.string().nullable(),
protectedKeyTag: z.string().nullable(),
publicKey: z.string(),
encryptedPrivateKey: z.string(),
iv: z.string(),
tag: z.string(),
encryptionVersion: z.number().default(1).nullish(),
protectedKey: z.string().nullish(),
protectedKeyIV: z.string().nullish(),
protectedKeyTag: z.string().nullish(),
publicKey: z.string().nullish(),
encryptedPrivateKey: z.string().nullish(),
iv: z.string().nullish(),
tag: z.string().nullish(),
token: z.string()
})
}
@@ -181,4 +181,59 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
} as const;
}
});
// New login route that doesn't use SRP
server.route({
method: "POST",
url: "/login",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
email: z.string().trim(),
password: z.string().trim(),
providerAuthToken: z.string().trim().optional(),
captchaToken: z.string().trim().optional()
}),
response: {
200: z.object({
accessToken: z.string()
})
}
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
if (!userAgent) throw new Error("user agent header is required");
const { tokens } = await server.services.login.login({
email: req.body.email,
password: req.body.password,
ip: req.realIp,
userAgent,
providerAuthToken: req.body.providerAuthToken,
captchaToken: req.body.captchaToken
});
const appCfg = getConfig();
void res.setCookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
return { accessToken: tokens.accessToken };
}
});
};
@@ -98,15 +98,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
email: z.string().trim(),
firstName: z.string().trim(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
publicKey: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
providerAuthToken: z.string().trim().optional().nullish(),
attributionSource: z.string().trim().optional(),
password: z.string()
@@ -189,15 +180,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
password: z.string(),
firstName: z.string().trim(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
publicKey: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
tokenMetadata: z.string().optional()
}),
response: {
@@ -35,7 +35,7 @@ export const getGitHubInstanceApiUrl = async (config: {
}) => {
const host = config.credentials.host || "github.com";
await blockLocalAndPrivateIpAddresses(host);
await blockLocalAndPrivateIpAddresses(`https://${host}`);
let apiBase: string;
if (config.credentials.instanceType === "server") {
@@ -44,7 +44,8 @@ const getConnectionConfig = ({
? {
trustServerCertificate: !sslRejectUnauthorized,
encrypt: true,
cryptoCredentialsDetails: sslCertificate ? { ca: sslCertificate } : {}
cryptoCredentialsDetails: sslCertificate ? { ca: sslCertificate } : {},
servername: host
}
: { encrypt: false }
};
+57 -2
View File
@@ -1,8 +1,16 @@
import { TUsers } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto } from "@app/lib/crypto";
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type";
import {
AuthMethod,
AuthModeProviderJwtTokenPayload,
AuthModeProviderSignUpTokenPayload,
AuthTokenType
} from "./auth-type";
export const validateProviderAuthToken = (providerToken: string, username?: string) => {
if (!providerToken) throw new UnauthorizedError();
@@ -97,3 +105,50 @@ export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?:
}
}
};
export const verifyCaptcha = async (user: TUsers, captchaToken?: string) => {
const appCfg = getConfig();
if (
user.consecutiveFailedPasswordAttempts &&
user.consecutiveFailedPasswordAttempts >= 10 &&
Boolean(appCfg.CAPTCHA_SECRET)
) {
if (!captchaToken) {
throw new BadRequestError({
name: "Captcha Required",
message: "Accomplish the required captcha by logging in via Web"
});
}
// validate captcha token
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
response: captchaToken,
secret: appCfg.CAPTCHA_SECRET
});
if (!response.data.success) {
throw new BadRequestError({
name: "Invalid Captcha"
});
}
}
};
export const getAuthMethodAndOrgId = (email: string, providerAuthToken?: string) => {
let authMethod = AuthMethod.EMAIL;
let organizationId: string | undefined;
if (providerAuthToken) {
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
authMethod = decodedProviderToken.authMethod;
if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
decodedProviderToken.orgId
) {
organizationId = decodedProviderToken.orgId;
}
}
return { authMethod, organizationId };
};
+86 -42
View File
@@ -4,7 +4,6 @@ import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSc
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
@@ -22,7 +21,8 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { LoginMethod } from "../super-admin/super-admin-types";
import { TTotpServiceFactory } from "../totp/totp-service";
import { TUserDALFactory } from "../user/user-dal";
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
import { UserEncryption } from "../user/user-types";
import { enforceUserLockStatus, getAuthMethodAndOrgId, validateProviderAuthToken, verifyCaptcha } from "./auth-fns";
import {
TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO,
@@ -208,6 +208,10 @@ export const authLoginServiceFactory = ({
throw new Error("Failed to find user");
}
if (!userEnc.salt || !userEnc.verifier) {
throw new BadRequestError({ message: "Salt or verifier not found" });
}
if (
serverCfg.enabledLoginMethods &&
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
@@ -247,8 +251,6 @@ export const authLoginServiceFactory = ({
captchaToken,
password
}: TLoginClientProofDTO) => {
const appCfg = getConfig();
// akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
@@ -259,44 +261,11 @@ export const authLoginServiceFactory = ({
const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig();
let authMethod = AuthMethod.EMAIL;
let organizationId: string | undefined;
const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken);
await verifyCaptcha(user, captchaToken);
if (providerAuthToken) {
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
authMethod = decodedProviderToken.authMethod;
if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
decodedProviderToken.orgId
) {
organizationId = decodedProviderToken.orgId;
}
}
if (
user.consecutiveFailedPasswordAttempts &&
user.consecutiveFailedPasswordAttempts >= 10 &&
Boolean(appCfg.CAPTCHA_SECRET)
) {
if (!captchaToken) {
throw new BadRequestError({
name: "Captcha Required",
message: "Accomplish the required captcha by logging in via Web"
});
}
// validate captcha token
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
response: captchaToken,
secret: appCfg.CAPTCHA_SECRET
});
if (!response.data.success) {
throw new BadRequestError({
name: "Invalid Captcha"
});
}
if (!userEnc.salt || !userEnc.verifier) {
throw new BadRequestError({ message: "Salt or verifier not found" });
}
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
@@ -371,6 +340,80 @@ export const authLoginServiceFactory = ({
return { token, user: userEnc } as const;
};
const login = async ({
email,
password,
ip,
userAgent,
providerAuthToken,
captchaToken
}: {
email: string;
password: string;
ip: string;
userAgent: string;
providerAuthToken?: string;
captchaToken?: string;
}) => {
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
});
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc) throw new BadRequestError({ message: "User not found" });
if (userEnc.encryptionVersion !== UserEncryption.V2) {
throw new BadRequestError({ message: "Legacy encryption scheme not supported", name: "LegacyEncryptionScheme" });
}
if (!userEnc.hashedPassword) {
if (userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
throw new BadRequestError({
message: "Legacy encryption scheme not supported",
name: "LegacyEncryptionScheme"
});
}
throw new BadRequestError({ message: "No password found" });
}
const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken);
await verifyCaptcha(userEnc, captchaToken);
if (!(await crypto.hashing().compareHash(password, userEnc.hashedPassword))) {
await userDAL.update(
{ id: userEnc.userId },
{
$incr: {
consecutiveFailedPasswordAttempts: 1
}
}
);
throw new BadRequestError({ message: "Invalid username or email" });
}
const token = await generateUserTokens({
user: {
...userEnc,
id: userEnc.userId
},
ip,
userAgent,
authMethod,
organizationId
});
return {
tokens: {
accessToken: token.access,
refreshToken: token.refresh
},
user: userEnc
} as const;
};
const selectOrganization = async ({
userAgent,
authJwtToken,
@@ -862,6 +905,7 @@ export const authLoginServiceFactory = ({
resendMfaToken,
verifyMfaToken,
selectOrganization,
generateUserTokens
generateUserTokens,
login
};
};
@@ -1,8 +1,5 @@
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { crypto } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
@@ -16,8 +13,6 @@ import { UserEncryption } from "../user/user-types";
import { TAuthDALFactory } from "./auth-dal";
import {
ResetPasswordV2Type,
TChangePasswordDTO,
TCreateBackupPrivateKeyDTO,
TResetPasswordV2DTO,
TResetPasswordViaBackupKeyDTO,
TSetupPasswordViaBackupKeyDTO
@@ -40,79 +35,6 @@ export const authPaswordServiceFactory = ({
smtpService,
totpConfigDAL
}: TAuthPasswordServiceFactoryDep) => {
/*
* Pre setup for pass change with srp protocol
* Gets srp server user salt and server public key
*/
const generateServerPubKey = async (userId: string, clientPublicKey: string) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user");
const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
const userEncKeys = await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
clientPublicKey,
serverPrivateKey: serverSrpKey.privateKey
});
if (!userEncKeys) throw new Error("Failed to update encryption key");
return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey };
};
/*
* Change password to new pass
* */
const changePassword = async ({
userId,
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
tokenVersionId,
password
}: TChangePasswordDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user");
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
const isValidClientProof = await srpCheckClientProof(
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
const appCfg = getConfig();
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
await userDAL.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
serverPrivateKey: null,
clientPublicKey: null,
hashedPassword
});
if (tokenVersionId) {
await tokenService.clearTokenSessionById(userEnc.userId, tokenVersionId);
}
};
/*
* Email password reset flow via email. Step 1 send email
*/
@@ -193,6 +115,10 @@ export const authPaswordServiceFactory = ({
}
if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
logger.error(
{ authMethods: user.authMethods },
"Unable to reset password, no email authentication method is configured"
);
throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
}
@@ -211,58 +137,17 @@ export const authPaswordServiceFactory = ({
}
}
const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS);
// we need to get the original private key first for v2
let privateKey: string;
if (
user.serverEncryptedPrivateKey &&
user.serverEncryptedPrivateKeyTag &&
user.serverEncryptedPrivateKeyIV &&
user.serverEncryptedPrivateKeyEncoding &&
user.encryptionVersion === UserEncryption.V2
) {
privateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
iv: user.serverEncryptedPrivateKeyIV,
tag: user.serverEncryptedPrivateKeyTag,
ciphertext: user.serverEncryptedPrivateKey,
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
} else {
if (user.encryptionVersion !== UserEncryption.V2) {
throw new BadRequestError({
message: "Cannot reset password without current credentials or recovery method",
name: "Reset password"
});
}
const encKeys = await generateUserSrpKeys(user.username, newPassword, {
publicKey: user.publicKey,
privateKey
});
const { tag, iv, ciphertext, encoding } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS);
await userDAL.updateUserEncryptionByUserId(userId, {
hashedPassword: newHashedPassword,
// srp params
salt: encKeys.salt,
verifier: encKeys.verifier,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
serverEncryptedPrivateKey: ciphertext,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyEncoding: encoding
hashedPassword: newHashedPassword
});
await tokenService.revokeAllMySessions(userId);
@@ -313,66 +198,6 @@ export const authPaswordServiceFactory = ({
});
};
/*
* backup key creation to give user's their access back when lost their password
* this also needs to do the generateServerPubKey function to be executed first
* then only client proof can be verified
* */
const createBackupPrivateKey = async ({
clientProof,
encryptedPrivateKey,
salt,
verifier,
iv,
tag,
userId
}: TCreateBackupPrivateKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key");
const isValidClientProff = await srpCheckClientProof(
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProff) throw new Error("failed to create backup key");
const backup = await authDAL.transaction(async (tx) => {
const backupKey = await authDAL.upsertBackupKey(
userEnc.userId,
{
encryptedPrivateKey,
iv,
tag,
salt,
verifier,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
},
tx
);
await userDAL.updateUserEncryptionByUserId(
userEnc.userId,
{
serverPrivateKey: null,
clientPublicKey: null
},
tx
);
return backupKey;
});
return backup;
};
/*
* Return user back up
* */
const getBackupPrivateKeyOfUser = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user || (user && !user.isAccepted)) {
@@ -416,21 +241,7 @@ export const authPaswordServiceFactory = ({
});
};
const setupPassword = async (
{
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
password,
token
}: TSetupPasswordViaBackupKeyDTO,
actor: OrgServiceActor
) => {
const setupPassword = async ({ password, token }: TSetupPasswordViaBackupKeyDTO, actor: OrgServiceActor) => {
try {
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
@@ -466,15 +277,7 @@ export const authPaswordServiceFactory = ({
await userDAL.updateUserEncryptionByUserId(
actor.id,
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
encryptionVersion: UserEncryption.V2,
hashedPassword,
serverPrivateKey: null,
clientPublicKey: null
@@ -487,12 +290,9 @@ export const authPaswordServiceFactory = ({
};
return {
generateServerPubKey,
changePassword,
resetPasswordByBackupKey,
sendPasswordResetEmail,
verifyPasswordResetEmail,
createBackupPrivateKey,
getBackupPrivateKeyOfUser,
sendPasswordSetupEmail,
setupPassword,
@@ -1,18 +1,3 @@
export type TChangePasswordDTO = {
userId: string;
clientProof: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
tokenVersionId?: string;
password: string;
};
export enum ResetPasswordV2Type {
Recovery = "recovery",
LoggedInReset = "logged-in-reset"
@@ -39,14 +24,6 @@ export type TResetPasswordViaBackupKeyDTO = {
};
export type TSetupPasswordViaBackupKeyDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string;
token: string;
};
+18 -194
View File
@@ -1,11 +1,10 @@
import { OrgMembershipStatus, SecretKeyEncoding, TableName } from "@app/db/schemas";
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { getMinExpiresIn } from "@app/lib/fn";
import { isDisposableEmail } from "@app/lib/validator";
@@ -41,7 +40,7 @@ type TAuthSignupDep = {
| "findUserGroupMembershipsInProject"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
@@ -147,17 +146,8 @@ export const authSignupServiceFactory = ({
firstName,
lastName,
providerAuthToken,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
organizationName,
// attributionSource,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
ip,
userAgent,
authorization,
@@ -191,98 +181,18 @@ export const authSignupServiceFactory = ({
}
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
const privateKey = await getUserPrivateKey(password, {
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
encryptionVersion: UserEncryption.V2
});
const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found");
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
let userEncKey;
// below condition is true means this is system generated credentials
// the private key is actually system generated password
// thus we will re-encrypt the system generated private key with the new password
// akhilmhdh: you may find this like why? The reason is simple we are moving away from e2ee and these are pieces of it
// without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things
if (
systemGeneratedUserEncryptionKey &&
!systemGeneratedUserEncryptionKey.hashedPassword &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
) {
// get server generated password
const serverGeneratedPassword = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey
});
const encKeys = await generateUserSrpKeys(email, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey
});
// now reencrypt server generated key with user provided password
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
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,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
} else {
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
}
const userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
hashedPassword
},
tx
);
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
if (
@@ -400,19 +310,10 @@ export const authSignupServiceFactory = ({
const completeAccountInvite = async ({
email,
ip,
salt,
password,
verifier,
firstName,
publicKey,
userAgent,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
authorization
}: TCompleteAccountInviteDTO) => {
const sanitizedEmail = email.trim().toLowerCase();
@@ -437,94 +338,17 @@ export const authSignupServiceFactory = ({
const appCfg = getConfig();
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
const privateKey = await getUserPrivateKey(password, {
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
encryptionVersion: 2
});
const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found");
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
let userEncKey;
// this means this is system generated credentials
// now replace the private key
if (
systemGeneratedUserEncryptionKey &&
!systemGeneratedUserEncryptionKey.hashedPassword &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
) {
// get server generated password
const serverGeneratedPassword = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey
});
const encKeys = await generateUserSrpKeys(sanitizedEmail, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey
});
// now reencrypt server generated key with user provided password
userEncKey = await userDAL.upsertUserEncryptionKey(
us.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,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
} else {
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
}
const userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: 2,
hashedPassword
},
tx
);
const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
@@ -3,15 +3,6 @@ export type TCompleteAccountSignupDTO = {
password: string;
firstName: string;
lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName?: string;
providerAuthToken?: string | null;
attributionSource?: string | undefined;
@@ -26,15 +17,6 @@ export type TCompleteAccountInviteDTO = {
password: string;
firstName: string;
lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
ip: string;
userAgent: string;
authorization: string;
@@ -2,6 +2,7 @@ import { z } from "zod";
import { isValidIp } from "@app/lib/ip";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TAltNameMapping, TAltNameType } from "@app/services/certificate/certificate-types";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
@@ -56,3 +57,19 @@ export const validateAltNamesField = z
message: "Each alt name must be a valid hostname, email address, IP address or URL"
}
);
export const validateAndMapAltNameType = (name: string): TAltNameMapping | null => {
if (isFQDN(name, { allow_wildcard: true, require_tld: false })) {
return { type: TAltNameType.DNS, value: name };
}
if (z.string().url().safeParse(name).success) {
return { type: TAltNameType.URL, value: name };
}
if (z.string().email().safeParse(name).success) {
return { type: TAltNameType.EMAIL, value: name };
}
if (isValidIp(name)) {
return { type: TAltNameType.IP, value: name };
}
return null;
};
@@ -1,7 +1,6 @@
/* eslint-disable no-bitwise */
import * as x509 from "@peculiar/x509";
import RE2 from "re2";
import { z } from "zod";
import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
@@ -9,7 +8,6 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
@@ -17,7 +15,8 @@ import {
CertExtendedKeyUsage,
CertKeyAlgorithm,
CertKeyUsage,
CertStatus
CertStatus,
TAltNameMapping
} from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -34,6 +33,7 @@ import {
} from "../certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types";
import { validateAndMapAltNameType } from "../certificate-authority-validators";
type TInternalCertificateAuthorityFnsDeps = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
@@ -152,27 +152,15 @@ export const InternalCertificateAuthorityFns = ({
extensions.push(extendedKeyUsagesExtension);
}
let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = [];
let altNamesArray: TAltNameMapping[] = [];
if (subscriber.subjectAlternativeNames?.length) {
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
if (z.string().email().safeParse(altName).success) {
return { type: "email", value: altName };
const altNameType = validateAndMapAltNameType(altName);
if (!altNameType) {
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
}
if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) {
return { type: "dns", value: altName };
}
if (z.string().url().safeParse(altName).success) {
return { type: "url", value: altName };
}
if (z.string().ip().safeParse(altName).success) {
return { type: "ip", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
return altNameType;
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -426,27 +414,15 @@ export const InternalCertificateAuthorityFns = ({
);
}
let altNamesArray: { type: "email" | "dns" | "ip" | "url"; value: string }[] = [];
let altNamesArray: TAltNameMapping[] = [];
if (altNames) {
altNamesArray = altNames.split(",").map((altName) => {
if (z.string().email().safeParse(altName).success) {
return { type: "email", value: altName };
const altNameType = validateAndMapAltNameType(altName);
if (!altNameType) {
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
}
if (isFQDN(altName, { allow_wildcard: true, require_tld: false })) {
return { type: "dns", value: altName };
}
if (z.string().url().safeParse(altName).success) {
return { type: "url", value: altName };
}
if (z.string().ip().safeParse(altName).success) {
return { type: "ip", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
return altNameType;
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -2,7 +2,6 @@
import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
@@ -18,7 +17,6 @@ import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -34,7 +32,8 @@ import {
CertExtendedKeyUsageOIDToName,
CertKeyAlgorithm,
CertKeyUsage,
CertStatus
CertStatus,
TAltNameMapping
} from "../../certificate/certificate-types";
import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal";
import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns";
@@ -69,6 +68,7 @@ import {
TSignIntermediateDTO,
TUpdateCaDTO
} from "./internal-certificate-authority-types";
import { validateAndMapAltNameType } from "../certificate-authority-validators";
type TInternalCertificateAuthorityServiceFactoryDep = {
certificateAuthorityDAL: Pick<
@@ -1364,34 +1364,18 @@ export const internalCertificateAuthorityServiceFactory = ({
);
}
let altNamesArray: {
type: "email" | "dns";
value: string;
}[] = [];
let altNamesArray: TAltNameMapping[] = [];
if (altNames) {
altNamesArray = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
// check if the altName is a valid email
if (z.string().email().safeParse(altName).success) {
return {
type: "email",
value: altName
};
.map((altName): TAltNameMapping => {
const altNameType = validateAndMapAltNameType(altName);
if (!altNameType) {
throw new Error(`Invalid altName: ${altName}`);
}
// check if the altName is a valid hostname
if (isFQDN(altName, { allow_wildcard: true })) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
return altNameType;
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -1766,34 +1750,22 @@ export const internalCertificateAuthorityServiceFactory = ({
}
let altNamesFromCsr: string = "";
let altNamesArray: {
type: "email" | "dns";
value: string;
}[] = [];
let altNamesArray: TAltNameMapping[] = [];
if (altNames) {
altNamesArray = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
// check if the altName is a valid email
if (z.string().email().safeParse(altName).success) {
return {
type: "email",
value: altName
};
.map((altName): TAltNameMapping => {
const altNameType = validateAndMapAltNameType(altName);
if (!altNameType) {
throw new Error(`Invalid altName: ${altName}`);
}
// check if the altName is a valid hostname
if (isFQDN(altName, { allow_wildcard: true })) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
return altNameType;
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
} else {
// attempt to read from CSR if altNames is not explicitly provided
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
@@ -1801,11 +1773,16 @@ export const internalCertificateAuthorityServiceFactory = ({
const sanNames = new x509.GeneralNames(sanExtension.value);
altNamesArray = sanNames.items
.filter((value) => value.type === "email" || value.type === "dns")
.map((name) => ({
type: name.type as "email" | "dns",
value: name.value
}));
.filter(
(value) => value.type === "email" || value.type === "dns" || value.type === "url" || value.type === "ip"
)
.map((name): TAltNameMapping => {
const altNameType = validateAndMapAltNameType(name.value);
if (!altNameType) {
throw new Error(`Invalid altName from CSR: ${name.value}`);
}
return altNameType;
});
altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
}
@@ -104,3 +104,14 @@ export type TGetCertificateCredentialsDTO = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export enum TAltNameType {
EMAIL = "email",
DNS = "dns",
IP = "ip",
URL = "url"
}
export type TAltNameMapping = {
type: TAltNameType;
value: string;
};
@@ -1,6 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import { ActionProjectType, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types";
import {
constructPermissionErrorMessage,
@@ -188,7 +188,7 @@ export const groupProjectServiceFactory = ({
// other groups that are in the project
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx);
if (groupMembers.length) {
if (groupMembers.length && (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2)) {
const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx);
if (!ghostUser) {
@@ -205,6 +205,12 @@ export const groupProjectServiceFactory = ({
});
}
if (!ghostUserLatestKey.sender.publicKey) {
throw new NotFoundError({
message: `Failed to find project owner's latest key in project with name ${project.name}`
});
}
const bot = await projectBotDAL.findOne({ projectId: project.id }, tx);
if (!bot) {
@@ -231,6 +237,12 @@ export const groupProjectServiceFactory = ({
});
const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
if (!publicKey) {
throw new NotFoundError({
message: `Failed to find user's public key in project with name ${project.name}`
});
}
const { ciphertext: encryptedKey, nonce } = crypto
.encryption()
.asymmetric()
@@ -2,9 +2,14 @@
import { ForbiddenError } from "@casl/ability";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TIdentityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template";
import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
OrgPermissionIdentityActions,
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
@@ -44,6 +49,7 @@ type TIdentityLdapAuthServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: TKmsServiceFactory;
identityDAL: TIdentityDALFactory;
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
};
export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>;
@@ -55,7 +61,8 @@ export const identityLdapAuthServiceFactory = ({
identityOrgMembershipDAL,
licenseService,
permissionService,
kmsService
kmsService,
identityAuthTemplateDAL
}: TIdentityLdapAuthServiceFactoryDep) => {
const getLdapConfig = async (identityId: string) => {
const identity = await identityDAL.findOne({ id: identityId });
@@ -173,6 +180,7 @@ export const identityLdapAuthServiceFactory = ({
const attachLdapAuth = async ({
identityId,
templateId,
url,
searchBase,
searchFilter,
@@ -213,6 +221,14 @@ export const identityLdapAuthServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
if (templateId) {
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
}
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
if (!plan.ldap) {
@@ -241,33 +257,55 @@ export const identityLdapAuthServiceFactory = ({
if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
const identityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => {
const { encryptor } = await kmsService.createCipherPairWithDataKey({
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identityMembershipOrg.orgId
});
const template = templateId
? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId)
: undefined;
let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string; ldapCaCertificate?: string };
if (template) {
ldapConfig = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString());
} else {
if (!bindDN || !bindPass || !searchBase || !url) {
throw new BadRequestError({
message: "Invalid request. Missing bind DN, bind pass, search base, or URL."
});
}
ldapConfig = {
bindDN,
bindPass,
searchBase,
url,
ldapCaCertificate
};
}
const { cipherTextBlob: encryptedBindPass } = encryptor({
plainText: Buffer.from(bindPass)
plainText: Buffer.from(ldapConfig.bindPass)
});
const { cipherTextBlob: encryptedBindDN } = encryptor({
plainText: Buffer.from(ldapConfig.bindDN)
});
let encryptedLdapCaCertificate: Buffer | undefined;
if (ldapCaCertificate) {
if (ldapConfig.ldapCaCertificate) {
const { cipherTextBlob: encryptedCertificate } = encryptor({
plainText: Buffer.from(ldapCaCertificate)
plainText: Buffer.from(ldapConfig.ldapCaCertificate)
});
encryptedLdapCaCertificate = encryptedCertificate;
}
const { cipherTextBlob: encryptedBindDN } = encryptor({
plainText: Buffer.from(bindDN)
});
const isConnected = await testLDAPConfig({
bindDN,
bindPass,
caCert: ldapCaCertificate || "",
url
bindDN: ldapConfig.bindDN,
bindPass: ldapConfig.bindPass,
caCert: ldapConfig.ldapCaCertificate || "",
url: ldapConfig.url
});
if (!isConnected) {
@@ -282,15 +320,16 @@ export const identityLdapAuthServiceFactory = ({
identityId: identityMembershipOrg.identityId,
encryptedBindDN,
encryptedBindPass,
searchBase,
searchBase: ldapConfig.searchBase,
searchFilter,
url,
url: ldapConfig.url,
encryptedLdapCaCertificate,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
templateId
},
tx
);
@@ -301,6 +340,7 @@ export const identityLdapAuthServiceFactory = ({
const updateLdapAuth = async ({
identityId,
templateId,
url,
searchBase,
searchFilter,
@@ -344,6 +384,13 @@ export const identityLdapAuthServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
if (templateId) {
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
}
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
if (!plan.ldap) {
@@ -371,33 +418,56 @@ export const identityLdapAuthServiceFactory = ({
if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
const { encryptor } = await kmsService.createCipherPairWithDataKey({
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identityMembershipOrg.orgId
});
const template = templateId
? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId)
: undefined;
let config: {
bindDN?: string;
bindPass?: string;
searchBase?: string;
url?: string;
ldapCaCertificate?: string;
};
if (template) {
config = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString());
} else {
config = {
bindDN,
bindPass,
searchBase,
url,
ldapCaCertificate
};
}
let encryptedBindPass: Buffer | undefined;
if (bindPass) {
if (config.bindPass) {
const { cipherTextBlob: bindPassCiphertext } = encryptor({
plainText: Buffer.from(bindPass)
plainText: Buffer.from(config.bindPass)
});
encryptedBindPass = bindPassCiphertext;
}
let encryptedLdapCaCertificate: Buffer | undefined;
if (ldapCaCertificate) {
if (config.ldapCaCertificate) {
const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({
plainText: Buffer.from(ldapCaCertificate)
plainText: Buffer.from(config.ldapCaCertificate)
});
encryptedLdapCaCertificate = ldapCaCertificateCiphertext;
}
let encryptedBindDN: Buffer | undefined;
if (bindDN) {
if (config.bindDN) {
const { cipherTextBlob: bindDNCiphertext } = encryptor({
plainText: Buffer.from(bindDN)
plainText: Buffer.from(config.bindDN)
});
encryptedBindDN = bindDNCiphertext;
@@ -406,10 +476,10 @@ export const identityLdapAuthServiceFactory = ({
const { ldapConfig } = await getLdapConfig(identityId);
const isConnected = await testLDAPConfig({
bindDN: bindDN || ldapConfig.bindDN,
bindPass: bindPass || ldapConfig.bindPass,
caCert: ldapCaCertificate || ldapConfig.caCert,
url: url || ldapConfig.url
bindDN: config.bindDN || ldapConfig.bindDN,
bindPass: config.bindPass || ldapConfig.bindPass,
caCert: config.ldapCaCertificate || ldapConfig.caCert,
url: config.url || ldapConfig.url
});
if (!isConnected) {
@@ -420,14 +490,15 @@ export const identityLdapAuthServiceFactory = ({
}
const updatedLdapAuth = await identityLdapAuthDAL.updateById(identityLdapAuth.id, {
url,
searchBase,
url: config.url,
searchBase: config.searchBase,
searchFilter,
encryptedBindDN,
encryptedBindPass,
encryptedLdapCaCertificate,
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
accessTokenMaxTTL,
templateId: template?.id || null,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
@@ -14,11 +14,12 @@ export type TAllowedFields = z.infer<typeof AllowedFieldsSchema>;
export type TAttachLdapAuthDTO = {
identityId: string;
url: string;
searchBase: string;
templateId?: string;
url?: string;
searchBase?: string;
searchFilter: string;
bindDN: string;
bindPass: string;
bindDN?: string;
bindPass?: string;
ldapCaCertificate?: string;
allowedFields?: TAllowedFields[];
accessTokenTTL: number;
@@ -30,6 +31,7 @@ export type TAttachLdapAuthDTO = {
export type TUpdateLdapAuthDTO = {
identityId: string;
templateId?: string;
url?: string;
searchBase?: string;
searchFilter?: string;
@@ -1,19 +1,16 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
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 { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
type TOrgAdminServiceFactoryDep = {
@@ -25,7 +22,6 @@ type TOrgAdminServiceFactoryDep = {
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
smtpService: Pick<TSmtpService, "sendMail">;
};
@@ -38,7 +34,6 @@ export const orgAdminServiceFactory = ({
projectMembershipDAL,
projectKeyDAL,
projectBotDAL,
userDAL,
projectUserMembershipRoleDAL,
smtpService
}: TOrgAdminServiceFactoryDep) => {
@@ -83,7 +78,7 @@ export const orgAdminServiceFactory = ({
actorAuthMethod,
projectId
}: TAccessProjectDTO) => {
const { permission, membership } = await permissionService.getOrgPermission(
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
@@ -98,8 +93,10 @@ export const orgAdminServiceFactory = ({
const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId });
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
if (project.version === ProjectVersion.V1) {
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
if (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2) {
throw new BadRequestError({
message: `Project '${project.name}' is a legacy project and must be upgraded before accessing it through the admin console.`
});
}
// check already there exist a membership if there return it
@@ -144,30 +141,6 @@ export const orgAdminServiceFactory = ({
});
}
const botPrivateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId);
if (!userEncryptionKey)
throw new NotFoundError({ message: `User encryption key for user with ID '${actorId}' not found` });
const [newWsMember] = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: [
{
orgMembershipId: membership.id,
userPublicKey: userEncryptionKey.publicKey
}
]
});
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
const newProjectMembership = await projectMembershipDAL.create(
{
@@ -181,16 +154,6 @@ export const orgAdminServiceFactory = ({
tx
);
await projectKeyDAL.create(
{
encryptedKey: newWsMember.workspaceEncryptedKey,
nonce: newWsMember.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: actorId,
projectId
},
tx
);
return newProjectMembership;
});
+2 -144
View File
@@ -8,7 +8,6 @@ import {
OrgMembershipStatus,
ProjectMembershipRole,
ProjectVersion,
SecretKeyEncoding,
TableName,
TProjectMemberships,
TProjectUserMembershipRolesInsert,
@@ -58,8 +57,6 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers, createProjectKey } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
@@ -137,7 +134,6 @@ type TOrgServiceFactoryDep = {
>;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
@@ -169,7 +165,6 @@ export const orgServiceFactory = ({
projectRoleDAL,
samlConfigDAL,
oidcConfigDAL,
projectBotDAL,
projectUserMembershipRoleDAL,
identityMetadataDAL,
projectBotService,
@@ -287,15 +282,7 @@ export const orgServiceFactory = ({
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
publicKey: encKeys.publicKey
},
tx
);
@@ -885,29 +872,10 @@ export const orgServiceFactory = ({
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) {
const serverGeneratedPassword = crypto.randomBytes(32).toString("hex");
const { tag, encoding, ciphertext, iv } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(serverGeneratedPassword);
const encKeys = await generateUserSrpKeys(inviteeEmail, serverGeneratedPassword);
await userDAL.createUserEncryption(
{
userId: inviteeUserId,
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,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
encryptionVersion: 2
},
tx
);
@@ -1069,106 +1037,6 @@ export const orgServiceFactory = ({
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
// this will auto generate bot
const { botKey, bot: autoGeneratedBot } = await projectBotService.getBotKey(projectId, true);
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
let ghostUserId = ghostUser?.id;
// backfill missing ghost user
if (!ghostUserId) {
const newGhostUser = await addGhostUser(project.orgId, tx);
const projectMembership = await projectMembershipDAL.create(
{
userId: newGhostUser.user.id,
projectId: project.id
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
tx
);
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: newGhostUser.keys.publicKey,
privateKey: newGhostUser.keys.plainPrivateKey,
plainProjectKey: botKey
});
// 4. Save the project key for the ghost user.
await projectKeyDAL.create(
{
projectId: project.id,
receiverId: newGhostUser.user.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: newGhostUser.user.id
},
tx
);
const { iv, tag, ciphertext, encoding, algorithm } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(newGhostUser.keys.plainPrivateKey);
if (autoGeneratedBot) {
await projectBotDAL.updateById(
autoGeneratedBot.id,
{
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: newGhostUser.keys.publicKey,
senderId: newGhostUser.user.id,
algorithm,
keyEncoding: encoding
},
tx
);
}
ghostUserId = newGhostUser.user.id;
}
const bot = await projectBotDAL.findOne({ projectId }, tx);
if (!bot) {
throw new NotFoundError({
name: "InviteUser",
message: `Failed to find project bot for project with ID '${projectId}'`
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUserId, projectId, tx);
if (!ghostUserLatestKey) {
throw new NotFoundError({
name: "InviteUser",
message: `Failed to find project owner's latest key for project with ID '${projectId}'`
});
}
const botPrivateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const newWsMembers = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
orgMembershipId: userEnc.userId,
projectMembershipRole: ProjectMembershipRole.Admin,
userPublicKey: userEnc.publicKey
}))
});
const projectMemberships = await projectMembershipDAL.insertMany(
userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
projectId,
@@ -1191,16 +1059,6 @@ export const orgServiceFactory = ({
});
await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
await projectKeyDAL.insertMany(
newWsMembers.map((el) => ({
encryptedKey: el.workspaceEncryptedKey,
nonce: el.workspaceEncryptedNonce,
senderId: ghostUserId,
receiverId: el.orgMembershipId,
projectId
})),
tx
);
mailsForProjectInvitation.push({
email: userWithEncryptionKeyInvitedToProject
.filter((el) => !userIdsWithOrgInvitation.has(el.userId))
@@ -42,6 +42,13 @@ export const getBotKeyFnFactory = (
message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console.`
});
}
if (!projectV1Keys.senderPublicKey) {
throw new NotFoundError({
message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console and upgrade the project.`
});
}
let userPrivateKey = "";
if (
projectV1Keys?.serverEncryptedPrivateKey &&
@@ -14,7 +14,7 @@ export const projectKeyDALFactory = (db: TDbClient) => {
userId: string,
projectId: string,
tx?: Knex
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => {
): Promise<(TProjectKeys & { sender: { publicKey?: string } }) | undefined> => {
try {
const projectKey = await (tx || db.replicaNode())(TableName.ProjectKeys)
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
@@ -25,7 +25,7 @@ export const projectKeyDALFactory = (db: TDbClient) => {
.select(db.ref("publicKey").withSchema(TableName.UserEncryptionKey))
.first();
if (projectKey) {
return { ...projectKey, sender: { publicKey: projectKey.publicKey } };
return { ...projectKey, sender: { publicKey: projectKey.publicKey || undefined } };
}
} catch (error) {
throw new DatabaseError({ error, name: "Find latest project key" });
@@ -10,6 +10,10 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types";
export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
if (!decryptKey.sender.publicKey) {
throw new Error("Decrypt key sender public key not found");
}
const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({
ciphertext: decryptKey.encryptedKey,
nonce: decryptKey.nonce,
@@ -121,6 +121,10 @@ export const projectQueueFactory = ({
tag: data.encryptedPrivateKey.encryptedKeyTag
});
if (!oldProjectKey.sender.publicKey) {
throw new Error("Old project key sender public key not found");
}
const decryptedPlainProjectKey = crypto.encryption().asymmetric().decrypt({
ciphertext: oldProjectKey.encryptedKey,
nonce: oldProjectKey.nonce,
@@ -290,6 +294,10 @@ export const projectQueueFactory = ({
continue;
}
if (!user.publicKey) {
throw new Error(`User with ID ${key.receiverId} has no public key during upgrade.`);
}
const [newMember] = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey,
+5 -100
View File
@@ -55,13 +55,10 @@ import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microso
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
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 { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
@@ -78,7 +75,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
import { TProjectDALFactory } from "./project-dal";
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
import { bootstrapSshProject } from "./project-fns";
import { TProjectQueueFactory } from "./project-queue";
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
import {
@@ -123,6 +120,7 @@ export const DEFAULT_PROJECT_ENVS = [
type TProjectServiceFactoryDep = {
projectDAL: TProjectDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "create">;
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">;
projectQueue: TProjectQueueFactory;
userDAL: TUserDALFactory;
@@ -132,9 +130,7 @@ type TProjectServiceFactoryDep = {
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: TIdentityProjectDALFactory;
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick<
TProjectMembershipDALFactory,
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
@@ -167,12 +163,10 @@ type TProjectServiceFactoryDep = {
sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">;
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
smtpService: Pick<TSmtpService, "sendMail">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">;
kmsService: Pick<
TKmsServiceFactory,
@@ -196,27 +190,25 @@ export const projectServiceFactory = ({
secretDAL,
secretV2BridgeDAL,
projectQueue,
projectKeyDAL,
permissionService,
projectBotService,
orgDAL,
userDAL,
folderDAL,
orgService,
identityProjectDAL,
identityOrgMembershipDAL,
projectMembershipDAL,
projectEnvDAL,
licenseService,
projectUserMembershipRoleDAL,
projectRoleDAL,
identityProjectMembershipRoleDAL,
certificateAuthorityDAL,
certificateDAL,
certificateTemplateDAL,
pkiCollectionDAL,
pkiAlertDAL,
pkiSubscriberDAL,
identityProjectDAL,
identityProjectMembershipRoleDAL,
sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL,
sshCertificateDAL,
@@ -225,7 +217,6 @@ export const projectServiceFactory = ({
sshHostGroupDAL,
keyStore,
kmsService,
projectBotDAL,
projectSlackConfigDAL,
projectMicrosoftTeamsConfigDAL,
slackIntegrationDAL,
@@ -253,7 +244,7 @@ export const projectServiceFactory = ({
type = ProjectType.SecretManager
}: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId });
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
organization.id,
@@ -277,7 +268,6 @@ export const projectServiceFactory = ({
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
});
}
const ghostUser = await orgService.addGhostUser(organization.id, tx);
if (kmsKeyId) {
const kms = await kmsService.getKmsById(kmsKeyId, tx);
@@ -329,19 +319,6 @@ export const projectServiceFactory = ({
});
}
// set ghost user as admin of project
const projectMembership = await projectMembershipDAL.create(
{
userId: ghostUser.user.id,
projectId: project.id
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
tx
);
// set default environments and root folder for provided environments
let envs: TProjectEnvironments[] = [];
if (projectTemplate) {
@@ -374,55 +351,6 @@ export const projectServiceFactory = ({
);
}
// 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 } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(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
@@ -432,17 +360,6 @@ export const projectServiceFactory = ({
throw new Error("User not found");
}
const [projectAdmin] = assignWorkspaceKeysToMembers({
decryptKey: latestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: orgMembership.id
}
]
});
// Create a membership for the user
const userProjectMembership = await projectMembershipDAL.create(
{
@@ -455,18 +372,6 @@ export const projectServiceFactory = ({
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
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
@@ -117,7 +117,7 @@ export type TUpgradeProjectDTO = {
} & TProjectPermission;
export type AddUserToWsDTO = {
decryptKey: TProjectKeys & { sender: { publicKey: string } };
decryptKey: TProjectKeys & { sender: { publicKey?: string } };
userPrivateKey: string;
members: {
orgMembershipId: string;
@@ -42,7 +42,7 @@ export const reminderServiceFactory = ({
const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise<void> => {
if (!newRecipients || newRecipients.length === 0) {
// If no recipients provided, remove all existing recipients
await reminderRecipientDAL.deleteById(reminderId);
await reminderRecipientDAL.delete({ reminderId });
return;
}
+5 -23
View File
@@ -1,9 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
@@ -175,7 +173,11 @@ export const userServiceFactory = ({
const getMe = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" });
return user;
return {
...user,
encryptionVersion: user.encryptionVersion!
};
};
const deleteUser = async (userId: string) => {
@@ -212,25 +214,6 @@ export const userServiceFactory = ({
);
};
const getUserPrivateKey = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user?.serverEncryptedPrivateKey || !user.serverEncryptedPrivateKeyIV || !user.serverEncryptedPrivateKeyTag) {
throw new NotFoundError({ message: `Private key for user with ID '${userId}' not found` });
}
const privateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
ciphertext: user.serverEncryptedPrivateKey,
tag: user.serverEncryptedPrivateKeyTag,
iv: user.serverEncryptedPrivateKeyIV,
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
return privateKey;
};
const getUserProjectFavorites = async (userId: string, orgId: string) => {
const orgMembership = await orgMembershipDAL.findOne({
userId,
@@ -311,7 +294,6 @@ export const userServiceFactory = ({
listUserGroups,
getUserAction,
unlockUser,
getUserPrivateKey,
getAllMyAccounts,
getUserProjectFavorites,
removeMyDuplicateAccounts,