diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index c0e18a763..111f42520 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -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" ); diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 15e967948..adf9489d4 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -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 diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 185a32356..f645cb8f2 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -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, diff --git a/backend/src/db/migrations/20250723220500_remove-srp.ts b/backend/src/db/migrations/20250723220500_remove-srp.ts new file mode 100644 index 000000000..4eb93e86a --- /dev/null +++ b/backend/src/db/migrations/20250723220500_remove-srp.ts @@ -0,0 +1,18 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + 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 { + // do nothing for now to avoid breaking down migrations +} diff --git a/backend/src/db/migrations/20250801170240_add-identity-auth-template.ts b/backend/src/db/migrations/20250801170240_add-identity-auth-template.ts new file mode 100644 index 000000000..60974a661 --- /dev/null +++ b/backend/src/db/migrations/20250801170240_add-identity-auth-template.ts @@ -0,0 +1,36 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + 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 { + 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); +} diff --git a/backend/src/db/schemas/identity-auth-templates.ts b/backend/src/db/schemas/identity-auth-templates.ts new file mode 100644 index 000000000..efe8ccb8c --- /dev/null +++ b/backend/src/db/schemas/identity-auth-templates.ts @@ -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; +export type TIdentityAuthTemplatesInsert = Omit, TImmutableDBKeys>; +export type TIdentityAuthTemplatesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/identity-ldap-auths.ts b/backend/src/db/schemas/identity-ldap-auths.ts index e8d0658d5..3e4d88649 100644 --- a/backend/src/db/schemas/identity-ldap-auths.ts +++ b/backend/src/db/schemas/identity-ldap-auths.ts @@ -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; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 55ec12faa..855934b28 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -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", diff --git a/backend/src/db/schemas/user-encryption-keys.ts b/backend/src/db/schemas/user-encryption-keys.ts index fd9d21a9d..cfe61fdc5 100644 --- a/backend/src/db/schemas/user-encryption-keys.ts +++ b/backend/src/db/schemas/user-encryption-keys.ts @@ -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(), diff --git a/backend/src/db/seed-data.ts b/backend/src/db/seed-data.ts index 2aee85fb1..f1a70ca05 100644 --- a/backend/src/db/seed-data.ts +++ b/backend/src/db/seed-data.ts @@ -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, diff --git a/backend/src/db/seeds/1-user.ts b/backend/src/db/seeds/1-user.ts index 5c6245382..43ce4dadf 100644 --- a/backend/src/db/seeds/1-user.ts +++ b/backend/src/db/seeds/1-user.ts @@ -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 { 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 { { id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true } ]); // Inserts seed entries + const [user] = await knex(TableName.Users) .insert([ { diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts index 26f96eafb..47a41a95c 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -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 { // 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 { }) .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) diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index 391f785ec..ae3d04514 100644 --- a/backend/src/db/seeds/5-machine-identity.ts +++ b/backend/src/db/seeds/5-machine-identity.ts @@ -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 { 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([ { diff --git a/backend/src/ee/routes/v1/identity-template-router.ts b/backend/src/ee/routes/v1/identity-template-router.ts new file mode 100644 index 000000000..b30069643 --- /dev/null +++ b/backend/src/ee/routes/v1/identity-template-router.ts @@ -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; + } + }); +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 8f3b69dfa..ab9503f58 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -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" }); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 818bb1f99..11d045eb2 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -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; allowedFields?: TAllowedFields[]; url: string; + templateId?: string | null; }; } @@ -1338,6 +1366,7 @@ interface UpdateIdentityLdapAuthEvent { accessTokenTrustedIps?: Array; allowedFields?: TAllowedFields[]; url?: string; + templateId?: string | null; }; } @@ -3439,6 +3468,9 @@ export type Event = | UpdateIdentityEvent | DeleteIdentityEvent | LoginIdentityUniversalAuthEvent + | MachineIdentityAuthTemplateCreateEvent + | MachineIdentityAuthTemplateUpdateEvent + | MachineIdentityAuthTemplateDeleteEvent | AddIdentityUniversalAuthEvent | UpdateIdentityUniversalAuthEvent | DeleteIdentityUniversalAuthEvent diff --git a/backend/src/ee/services/event/event-sse-service.ts b/backend/src/ee/services/event/event-sse-service.ts index f8c025e77..bb85f667c 100644 --- a/backend/src/ee/services/event/event-sse-service.ts +++ b/backend/src/ee/services/event/event-sse-service.ts @@ -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(); - heartbeatInterval = setInterval(() => { + const heartbeatInterval = setInterval(() => { for (const client of clients) { if (client.stream.closed) continue; void client.ping(); diff --git a/backend/src/ee/services/event/event-sse-stream.ts b/backend/src/ee/services/event/event-sse-stream.ts index d73eccb42..258783c2e 100644 --- a/backend/src/ee/services/event/event-sse-stream.ts +++ b/backend/src/ee/services/event/event-sse-stream.ts @@ -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(); diff --git a/backend/src/ee/services/group/group-fns.ts b/backend/src/ee/services/group/group-fns.ts index 436b0b79e..56f8df6c0 100644 --- a/backend/src/ee/services/group/group-fns.ts +++ b/backend/src/ee/services/group/group-fns.ts @@ -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() diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 55665c146..46c9831b0 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = { TUserGroupMembershipDALFactory, "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find" >; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; projectKeyDAL: Pick; permissionService: Pick; diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index 1d7c5fc71..e91f7a47b 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -65,7 +65,7 @@ export type TAddUsersToGroup = { userGroupMembershipDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; tx: Knex; }; @@ -78,7 +78,7 @@ export type TAddUsersToGroupByUserIds = { orgDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; tx?: Knex; }; @@ -102,7 +102,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = { >; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; tx?: Knex; }; diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-dal.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-dal.ts new file mode 100644 index 000000000..736f0f033 --- /dev/null +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-dal.ts @@ -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; + +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 + }; +}; diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-enums.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-enums.ts new file mode 100644 index 000000000..c5b47b158 --- /dev/null +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-enums.ts @@ -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; diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts new file mode 100644 index 000000000..ef071742d --- /dev/null +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts @@ -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; + kmsService: Pick; + licenseService: Pick; + auditLogService: Pick; +}; + +export type TIdentityAuthTemplateServiceFactory = ReturnType; + +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; + } & Omit) => { + 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; + } & Omit) => { + 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 = {}; + + 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 + }; +}; diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-types.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-types.ts new file mode 100644 index 000000000..8039e41c2 --- /dev/null +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-types.ts @@ -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; + +export type TUpdateIdentityAuthTemplateDTO = { + templateId: string; + name?: string; + templateFields?: Partial; +} & Omit; + +export type TDeleteIdentityAuthTemplateDTO = { + templateId: string; +} & Omit; + +export type TGetIdentityAuthTemplateDTO = { + templateId: string; +} & Omit; + +export type TListIdentityAuthTemplatesDTO = { + limit?: number; + offset?: number; + search?: string; +} & Omit; + +export type TGetTemplatesByAuthMethodDTO = { + authMethod: string; +} & Omit; + +export type TFindTemplateUsagesDTO = { + templateId: string; +} & Omit; + +export type TUnlinkTemplateUsageDTO = { + templateId: string; + identityIds: string[]; +} & Omit; + +// Specific LDAP types for convenience +export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO; +export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO; diff --git a/backend/src/ee/services/identity-auth-template/index.ts b/backend/src/ee/services/identity-auth-template/index.ts new file mode 100644 index 000000000..4358d1a27 --- /dev/null +++ b/backend/src/ee/services/identity-auth-template/index.ts @@ -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"; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index f3aac5b92..b1a10056c 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -55,7 +55,7 @@ type TLdapConfigServiceFactoryDep = { groupDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, diff --git a/backend/src/ee/services/license/__mocks__/license-fns.ts b/backend/src/ee/services/license/__mocks__/license-fns.ts index 5259d4616..4c42ad7a2 100644 --- a/backend/src/ee/services/license/__mocks__/license-fns.ts +++ b/backend/src/ee/services/license/__mocks__/license-fns.ts @@ -31,7 +31,8 @@ export const getDefaultOnPremFeatures = () => { caCrl: false, sshHostGroups: false, enterpriseSecretSyncs: false, - enterpriseAppConnections: false + enterpriseAppConnections: false, + machineIdentityAuthTemplates: false }; }; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index fecba7ba7..bd3949a7e 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -60,7 +60,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ enterpriseSecretSyncs: false, enterpriseAppConnections: false, fips: false, - eventSubscriptions: false + eventSubscriptions: false, + machineIdentityAuthTemplates: false }); export const setupLicenseRequestWithStore = ( diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 84a652c46..098d00feb 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -75,6 +75,7 @@ export type TFeatureSet = { secretScanning: false; enterpriseSecretSyncs: false; enterpriseAppConnections: false; + machineIdentityAuthTemplates: false; fips: false; eventSubscriptions: false; }; diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index ca6f67235..101f744c3 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -79,7 +79,7 @@ type TOidcConfigServiceFactoryDep = { >; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; auditLogService: Pick; kmsService: Pick; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index f0fe73d71..2436dae2a 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -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; }; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index a87d67b94..9cc6e134d 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -59,7 +59,7 @@ type TScimServiceFactoryDep = { TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById" | "update" >; - projectDAL: Pick; + projectDAL: Pick; projectMembershipDAL: Pick; groupDAL: Pick< TGroupDALFactory, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 72b283a0f..0cc272f15 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -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" + } +}; diff --git a/backend/src/lib/crypto/secret-encryption.ts b/backend/src/lib/crypto/secret-encryption.ts index 7355d4bd6..f22c918ac 100644 --- a/backend/src/lib/crypto/secret-encryption.ts +++ b/backend/src/lib/crypto/secret-encryption.ts @@ -53,7 +53,7 @@ type DecryptedIntegrationAuths = z.infer 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, diff --git a/backend/src/lib/crypto/srp.ts b/backend/src/lib/crypto/srp.ts index 3f403405e..2623e5756 100644 --- a/backend/src/lib/crypto/srp.ts +++ b/backend/src/lib/crypto/srp.ts @@ -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, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index ee0b2e0f3..64cfb140e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -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, diff --git a/backend/src/server/routes/v1/event-router.ts b/backend/src/server/routes/v1/event-router.ts index b8a043db5..19dc296bf 100644 --- a/backend/src/server/routes/v1/event-router.ts +++ b/backend/src/server/routes/v1/event-router.ts @@ -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 } }); } diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index 3da8a425b..5d3612bf5 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -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() }) }) } diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index e1669c784..323354bc1 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -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 }) diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index 3396cebe9..72d20db57 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -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() }), diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 6f981f61f..7a5a9341f 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -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() diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index a0c3592f7..7ef2e0d33 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -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", diff --git a/backend/src/server/routes/v2/mfa-router.ts b/backend/src/server/routes/v2/mfa-router.ts index 59a3943f7..d8a57d29a 100644 --- a/backend/src/server/routes/v2/mfa-router.ts +++ b/backend/src/server/routes/v2/mfa-router.ts @@ -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() }) } diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 87e1d1789..9135d2356 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -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 }) }) } diff --git a/backend/src/server/routes/v2/password-router.ts b/backend/src/server/routes/v2/password-router.ts index 63b6d8aac..0f0747c3f 100644 --- a/backend/src/server/routes/v2/password-router.ts +++ b/backend/src/server/routes/v2/password-router.ts @@ -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 + }); } }); }; diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 2a883dcb6..b71a744fd 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -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() }) }) ) diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 3a8510f34..07923fd6c 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -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 }; + } + }); }; diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 391c459a2..736bc13aa 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -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: { diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index f93ffc56d..e4bd2e10d 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -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") { diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts index 4c671986f..b9425d7be 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts @@ -44,7 +44,8 @@ const getConnectionConfig = ({ ? { trustServerCertificate: !sslRejectUnauthorized, encrypt: true, - cryptoCredentialsDetails: sslCertificate ? { ca: sslCertificate } : {} + cryptoCredentialsDetails: sslCertificate ? { ca: sslCertificate } : {}, + servername: host } : { encrypt: false } }; diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index b38275c8b..d490486cb 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -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 }; +}; diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index aea90fe11..7dd1d3aa4 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -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 }; }; diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 0dbdfaf79..efc8b3cc0 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -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, diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts index b3b14c3b4..ceb70d411 100644 --- a/backend/src/services/auth/auth-password-type.ts +++ b/backend/src/services/auth/auth-password-type.ts @@ -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; }; diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 96cf4121a..7bc3a5ef6 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -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; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; groupProjectDAL: Pick; orgService: Pick; @@ -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 }, diff --git a/backend/src/services/auth/auth-signup-type.ts b/backend/src/services/auth/auth-signup-type.ts index 8bbf302c5..5c4fdc3c9 100644 --- a/backend/src/services/auth/auth-signup-type.ts +++ b/backend/src/services/auth/auth-signup-type.ts @@ -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; diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index 3fa7e8d5e..908741bde 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -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; +}; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts index eace4e47a..6811476f8 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts @@ -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; @@ -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); diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index dd30cc62e..510a160e9 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -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(","); } diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index f1c79a36f..527df2a39 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -104,3 +104,14 @@ export type TGetCertificateCredentialsDTO = { projectDAL: Pick; kmsService: Pick; }; + +export enum TAltNameType { + EMAIL = "email", + DNS = "dns", + IP = "ip", + URL = "url" +} +export type TAltNameMapping = { + type: TAltNameType; + value: string; +}; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 50a08e94f..838d89437 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -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() diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 399ef7da9..f38f53cf0 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -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; kmsService: TKmsServiceFactory; identityDAL: TIdentityDALFactory; + identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory; }; export type TIdentityLdapAuthServiceFactory = ReturnType; @@ -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 diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts index 0e6feb5fb..8629763bb 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts @@ -14,11 +14,12 @@ export type TAllowedFields = z.infer; 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; diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 6640412c3..3dec0c7f8 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -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; projectBotDAL: Pick; - userDAL: Pick; projectUserMembershipRoleDAL: Pick; smtpService: Pick; }; @@ -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; }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index ca51ff57a..5779fa7d3 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -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; projectRoleDAL: Pick; - projectBotDAL: Pick; projectUserMembershipRoleDAL: Pick; projectBotService: Pick; loginService: Pick; @@ -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)) diff --git a/backend/src/services/project-bot/project-bot-fns.ts b/backend/src/services/project-bot/project-bot-fns.ts index d26669b86..029f320df 100644 --- a/backend/src/services/project-bot/project-bot-fns.ts +++ b/backend/src/services/project-bot/project-bot-fns.ts @@ -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 && diff --git a/backend/src/services/project-key/project-key-dal.ts b/backend/src/services/project-key/project-key-dal.ts index ea4ed813c..bb91b9c85 100644 --- a/backend/src/services/project-key/project-key-dal.ts +++ b/backend/src/services/project-key/project-key-dal.ts @@ -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" }); diff --git a/backend/src/services/project/project-fns.ts b/backend/src/services/project/project-fns.ts index f166ec04f..fca0074d7 100644 --- a/backend/src/services/project/project-fns.ts +++ b/backend/src/services/project/project-fns.ts @@ -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, diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts index 0d7c7dd55..e557b7c96 100644 --- a/backend/src/services/project/project-queue.ts +++ b/backend/src/services/project/project-queue.ts @@ -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, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 153f627fc..4261870b1 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -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; projectSshConfigDAL: Pick; projectQueue: TProjectQueueFactory; userDAL: TUserDALFactory; @@ -132,9 +130,7 @@ type TProjectServiceFactoryDep = { secretV2BridgeDAL: Pick; projectEnvDAL: Pick; identityOrgMembershipDAL: TIdentityOrgDALFactory; - identityProjectDAL: TIdentityProjectDALFactory; identityProjectMembershipRoleDAL: Pick; - projectKeyDAL: Pick; projectMembershipDAL: Pick< TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" @@ -167,12 +163,10 @@ type TProjectServiceFactoryDep = { sshHostDAL: Pick; sshHostGroupDAL: Pick; permissionService: TPermissionServiceFactory; - orgService: Pick; licenseService: Pick; smtpService: Pick; orgDAL: Pick; keyStore: Pick; - projectBotDAL: Pick; projectRoleDAL: Pick; 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 diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 02f89bc38..ceef78f6a 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -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; diff --git a/backend/src/services/reminder/reminder-service.ts b/backend/src/services/reminder/reminder-service.ts index 41bdf2be8..86788c249 100644 --- a/backend/src/services/reminder/reminder-service.ts +++ b/backend/src/services/reminder/reminder-service.ts @@ -42,7 +42,7 @@ export const reminderServiceFactory = ({ const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise => { if (!newRecipients || newRecipients.length === 0) { // If no recipients provided, remove all existing recipients - await reminderRecipientDAL.deleteById(reminderId); + await reminderRecipientDAL.delete({ reminderId }); return; } diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b2258447e..30bf750c3 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -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, diff --git a/docs/api-reference/endpoints/events/project-events.mdx b/docs/api-reference/endpoints/events/project-events.mdx new file mode 100644 index 000000000..9ceb0439e --- /dev/null +++ b/docs/api-reference/endpoints/events/project-events.mdx @@ -0,0 +1,4 @@ +--- +title: "Project Events" +openapi: "POST /api/v1/events/subscribe/project-events" +--- \ No newline at end of file diff --git a/docs/docs.json b/docs/docs.json index 4459a67de..960c41727 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -14,93 +14,51 @@ "navigation": { "tabs": [ { - "tab": "Documentation", + "tab": "Platform", "groups": [ { "group": "Getting Started", "pages": [ + "documentation/getting-started/overview", "documentation/getting-started/introduction", { - "group": "Quickstart", - "pages": ["documentation/guides/local-development"] - }, - { - "group": "Guides", + "group": "Concepts", "pages": [ - "documentation/guides/introduction", - "documentation/guides/node", - "documentation/guides/python", - "documentation/guides/nextjs-vercel", - "documentation/guides/microsoft-power-apps", - "documentation/guides/organization-structure" + "documentation/getting-started/concepts/deployment-models", + "documentation/getting-started/concepts/platform-hierarchy", + "documentation/getting-started/concepts/platform-iam", + "documentation/getting-started/concepts/client-integrations", + "documentation/getting-started/concepts/audit-logs" ] }, { - "group": "Setup", - "pages": ["documentation/setup/networking"] + "group": "Guides", + "pages": ["documentation/guides/organization-structure"] } ] }, { - "group": "Platform", + "group": "Platform Reference", "pages": [ "documentation/platform/organization", - "documentation/platform/project", + "documentation/platform/event-subscriptions", "documentation/platform/folder", { - "group": "Secrets", + "group": "Projects", "pages": [ - "documentation/platform/secret-versioning", - "documentation/platform/pit-recovery", - "documentation/platform/secret-reference", - "documentation/platform/webhooks" - ] - }, - { - "group": "Internal PKI", - "pages": [ - "documentation/platform/pki/overview", - "documentation/platform/pki/private-ca", - "documentation/platform/pki/external-ca", - "documentation/platform/pki/subscribers", - "documentation/platform/pki/certificates", - "documentation/platform/pki/acme-ca", - "documentation/platform/pki/est", - "documentation/platform/pki/alerting", + "documentation/platform/project", + "documentation/platform/project-templates", { - "group": "Integrations", + "group": "KMS Configuration", "pages": [ - "documentation/platform/pki/pki-issuer", - "documentation/platform/pki/integration-guides/gloo-mesh" + "documentation/platform/kms-configuration/overview", + "documentation/platform/kms-configuration/aws-kms", + "documentation/platform/kms-configuration/aws-hsm", + "documentation/platform/kms-configuration/gcp-kms" ] } ] }, - { - "group": "Infisical SSH", - "pages": [ - "documentation/platform/ssh/overview", - "documentation/platform/ssh/host-groups" - ] - }, - { - "group": "Key Management (KMS)", - "pages": [ - "documentation/platform/kms/overview", - "documentation/platform/kms/hsm-integration", - "documentation/platform/kms/kubernetes-encryption", - "documentation/platform/kms/kmip" - ] - }, - { - "group": "KMS Configuration", - "pages": [ - "documentation/platform/kms-configuration/overview", - "documentation/platform/kms-configuration/aws-kms", - "documentation/platform/kms-configuration/aws-hsm", - "documentation/platform/kms-configuration/gcp-kms" - ] - }, { "group": "Identities", "pages": [ @@ -140,57 +98,53 @@ ] }, { - "group": "Secret Rotation", + "group": "App Connections", "pages": [ - "documentation/platform/secret-rotation/overview", - "documentation/platform/secret-rotation/auth0-client-secret", - "documentation/platform/secret-rotation/aws-iam-user-secret", - "documentation/platform/secret-rotation/azure-client-secret", - "documentation/platform/secret-rotation/ldap-password", - "documentation/platform/secret-rotation/mssql-credentials", - "documentation/platform/secret-rotation/mysql-credentials", - "documentation/platform/secret-rotation/okta-client-secret", - "documentation/platform/secret-rotation/oracledb-credentials", - "documentation/platform/secret-rotation/postgres-credentials" + "integrations/app-connections/overview", + { + "group": "Connections", + "pages": [ + "integrations/app-connections/1password", + "integrations/app-connections/auth0", + "integrations/app-connections/aws", + "integrations/app-connections/azure-app-configuration", + "integrations/app-connections/azure-client-secrets", + "integrations/app-connections/azure-devops", + "integrations/app-connections/azure-key-vault", + "integrations/app-connections/bitbucket", + "integrations/app-connections/camunda", + "integrations/app-connections/checkly", + "integrations/app-connections/cloudflare", + "integrations/app-connections/databricks", + "integrations/app-connections/digital-ocean", + "integrations/app-connections/flyio", + "integrations/app-connections/gcp", + "integrations/app-connections/github", + "integrations/app-connections/github-radar", + "integrations/app-connections/gitlab", + "integrations/app-connections/hashicorp-vault", + "integrations/app-connections/heroku", + "integrations/app-connections/humanitec", + "integrations/app-connections/ldap", + "integrations/app-connections/mssql", + "integrations/app-connections/mysql", + "integrations/app-connections/netlify", + "integrations/app-connections/oci", + "integrations/app-connections/okta", + "integrations/app-connections/oracledb", + "integrations/app-connections/postgres", + "integrations/app-connections/railway", + "integrations/app-connections/render", + "integrations/app-connections/supabase", + "integrations/app-connections/teamcity", + "integrations/app-connections/terraform-cloud", + "integrations/app-connections/vercel", + "integrations/app-connections/windmill", + "integrations/app-connections/zabbix" + ] + } ] }, - { - "group": "Dynamic Secrets", - "pages": [ - "documentation/platform/dynamic-secrets/overview", - "documentation/platform/dynamic-secrets/aws-elasticache", - "documentation/platform/dynamic-secrets/aws-iam", - "documentation/platform/dynamic-secrets/azure-entra-id", - "documentation/platform/dynamic-secrets/cassandra", - "documentation/platform/dynamic-secrets/elastic-search", - "documentation/platform/dynamic-secrets/gcp-iam", - "documentation/platform/dynamic-secrets/github", - "documentation/platform/dynamic-secrets/ldap", - "documentation/platform/dynamic-secrets/mongo-atlas", - "documentation/platform/dynamic-secrets/mongo-db", - "documentation/platform/dynamic-secrets/mssql", - "documentation/platform/dynamic-secrets/mysql", - "documentation/platform/dynamic-secrets/oracle", - "documentation/platform/dynamic-secrets/postgresql", - "documentation/platform/dynamic-secrets/rabbit-mq", - "documentation/platform/dynamic-secrets/redis", - "documentation/platform/dynamic-secrets/sap-ase", - "documentation/platform/dynamic-secrets/sap-hana", - "documentation/platform/dynamic-secrets/snowflake", - "documentation/platform/dynamic-secrets/totp", - "documentation/platform/dynamic-secrets/kubernetes", - "documentation/platform/dynamic-secrets/vertica" - ] - }, - { - "group": "Gateway", - "pages": [ - "documentation/platform/gateways/overview", - "documentation/platform/gateways/gateway-security", - "documentation/platform/gateways/networking" - ] - }, - "documentation/platform/project-templates", { "group": "Workflow Integrations", "pages": [ @@ -206,22 +160,20 @@ "documentation/platform/external-migrations/vault" ] }, + "documentation/platform/admin-panel/server-admin", + "documentation/platform/secret-sharing" + ] + }, + { + "group": "Connectivity", + "pages": [ + "documentation/setup/networking", { - "group": "Admin Consoles", + "group": "Gateway", "pages": [ - "documentation/platform/admin-panel/overview", - "documentation/platform/admin-panel/server-admin", - "documentation/platform/admin-panel/org-admin-console" - ] - }, - "documentation/platform/secret-sharing", - { - "group": "Secret Scanning", - "pages": [ - "documentation/platform/secret-scanning/overview", - "documentation/platform/secret-scanning/bitbucket", - "documentation/platform/secret-scanning/github", - "documentation/platform/secret-scanning/gitlab" + "documentation/platform/gateways/overview", + "documentation/platform/gateways/gateway-security", + "documentation/platform/gateways/networking" ] } ] @@ -322,6 +274,7 @@ } ] }, + "documentation/platform/identities/auth-templates", "documentation/platform/token", "documentation/platform/mfa", "documentation/platform/github-org-sync" @@ -426,18 +379,80 @@ ] }, { - "tab": "Integrations", - "groups": [ + "tab": "Products", + "menu": [ { - "group": "Infrastructure Integrations", - "pages": [ - "integrations/platforms/ansible", - "integrations/platforms/apache-airflow", + "item": "Secrets Management", + "groups": [ { - "group": "Container orchestrators", + "group": "Secrets Management", "pages": [ + "documentation/platform/secrets-mgmt/overview", + "documentation/platform/secrets-mgmt/project", + "documentation/platform/folder", { - "group": "Kubernetes", + "group": "Secret Rotation", + "pages": [ + "documentation/platform/secret-rotation/overview", + "documentation/platform/secret-rotation/auth0-client-secret", + "documentation/platform/secret-rotation/aws-iam-user-secret", + "documentation/platform/secret-rotation/azure-client-secret", + "documentation/platform/secret-rotation/ldap-password", + "documentation/platform/secret-rotation/mssql-credentials", + "documentation/platform/secret-rotation/mysql-credentials", + "documentation/platform/secret-rotation/okta-client-secret", + "documentation/platform/secret-rotation/oracledb-credentials", + "documentation/platform/secret-rotation/postgres-credentials" + ] + }, + { + "group": "Dynamic Secrets", + "pages": [ + "documentation/platform/dynamic-secrets/overview", + "documentation/platform/dynamic-secrets/aws-elasticache", + "documentation/platform/dynamic-secrets/aws-iam", + "documentation/platform/dynamic-secrets/azure-entra-id", + "documentation/platform/dynamic-secrets/cassandra", + "documentation/platform/dynamic-secrets/elastic-search", + "documentation/platform/dynamic-secrets/gcp-iam", + "documentation/platform/dynamic-secrets/github", + "documentation/platform/dynamic-secrets/ldap", + "documentation/platform/dynamic-secrets/mongo-atlas", + "documentation/platform/dynamic-secrets/mongo-db", + "documentation/platform/dynamic-secrets/mssql", + "documentation/platform/dynamic-secrets/mysql", + "documentation/platform/dynamic-secrets/oracle", + "documentation/platform/dynamic-secrets/postgresql", + "documentation/platform/dynamic-secrets/rabbit-mq", + "documentation/platform/dynamic-secrets/redis", + "documentation/platform/dynamic-secrets/sap-ase", + "documentation/platform/dynamic-secrets/sap-hana", + "documentation/platform/dynamic-secrets/snowflake", + "documentation/platform/dynamic-secrets/totp", + "documentation/platform/dynamic-secrets/kubernetes", + "documentation/platform/dynamic-secrets/vertica" + ] + }, + { + "group": "Guides", + "pages": [ + "documentation/guides/introduction", + "documentation/guides/local-development", + "documentation/guides/node", + "documentation/guides/python", + "documentation/guides/nextjs-vercel", + "documentation/guides/microsoft-power-apps" + ] + } + ] + }, + { + "group": "Infrastructure Integrations", + "pages": [ + "integrations/platforms/ansible", + "integrations/platforms/apache-airflow", + { + "group": "Kubernetes Operator", "pages": [ "integrations/platforms/kubernetes/overview", "integrations/platforms/kubernetes/infisical-secret-crd", @@ -447,222 +462,249 @@ }, "integrations/platforms/kubernetes-injector", "integrations/platforms/kubernetes-csi", - "integrations/platforms/docker-swarm-with-agent", - "integrations/platforms/ecs-with-agent" + { + "group": "Agent", + "pages": [ + "integrations/platforms/infisical-agent", + "integrations/platforms/docker-swarm-with-agent", + "integrations/platforms/ecs-with-agent" + ] + }, + { + "group": "Docker", + "pages": [ + "integrations/platforms/docker-intro", + "integrations/platforms/docker", + "integrations/platforms/docker-pass-envs", + "integrations/platforms/docker-compose" + ] + }, + "integrations/frameworks/packer", + "integrations/frameworks/pulumi", + "integrations/frameworks/terraform" ] }, { - "group": "Docker", + "group": "Secret Syncs", "pages": [ - "integrations/platforms/docker-intro", - "integrations/platforms/docker", - "integrations/platforms/docker-pass-envs", - "integrations/platforms/docker-compose" + "integrations/secret-syncs/overview", + { + "group": "Syncs", + "pages": [ + "integrations/secret-syncs/1password", + "integrations/secret-syncs/aws-parameter-store", + "integrations/secret-syncs/aws-secrets-manager", + "integrations/secret-syncs/azure-app-configuration", + "integrations/secret-syncs/azure-devops", + "integrations/secret-syncs/azure-key-vault", + "integrations/secret-syncs/bitbucket", + "integrations/secret-syncs/camunda", + "integrations/secret-syncs/checkly", + "integrations/secret-syncs/cloudflare-pages", + "integrations/secret-syncs/cloudflare-workers", + "integrations/secret-syncs/databricks", + "integrations/secret-syncs/digital-ocean-app-platform", + "integrations/secret-syncs/flyio", + "integrations/secret-syncs/gcp-secret-manager", + "integrations/secret-syncs/github", + "integrations/secret-syncs/gitlab", + "integrations/secret-syncs/hashicorp-vault", + "integrations/secret-syncs/heroku", + "integrations/secret-syncs/humanitec", + "integrations/secret-syncs/netlify", + "integrations/secret-syncs/oci-vault", + "integrations/secret-syncs/railway", + "integrations/secret-syncs/render", + "integrations/secret-syncs/supabase", + "integrations/secret-syncs/teamcity", + "integrations/secret-syncs/terraform-cloud", + "integrations/secret-syncs/vercel", + "integrations/secret-syncs/windmill", + "integrations/secret-syncs/zabbix" + ] + } ] }, - "integrations/platforms/infisical-agent", - "integrations/frameworks/packer", - "integrations/frameworks/pulumi", - "integrations/frameworks/terraform" - ] - }, - { - "group": "App Connections", - "pages": [ - "integrations/app-connections/overview", { - "group": "Connections", + "group": "Native Integrations", "pages": [ - "integrations/app-connections/1password", - "integrations/app-connections/auth0", - "integrations/app-connections/aws", - "integrations/app-connections/azure-app-configuration", - "integrations/app-connections/azure-client-secrets", - "integrations/app-connections/azure-devops", - "integrations/app-connections/azure-key-vault", - "integrations/app-connections/bitbucket", - "integrations/app-connections/camunda", - "integrations/app-connections/checkly", - "integrations/app-connections/cloudflare", - "integrations/app-connections/databricks", - "integrations/app-connections/digital-ocean", - "integrations/app-connections/flyio", - "integrations/app-connections/gcp", - "integrations/app-connections/github", - "integrations/app-connections/github-radar", - "integrations/app-connections/gitlab", - "integrations/app-connections/hashicorp-vault", - "integrations/app-connections/heroku", - "integrations/app-connections/humanitec", - "integrations/app-connections/ldap", - "integrations/app-connections/mssql", - "integrations/app-connections/mysql", - "integrations/app-connections/netlify", - "integrations/app-connections/oci", - "integrations/app-connections/okta", - "integrations/app-connections/oracledb", - "integrations/app-connections/postgres", - "integrations/app-connections/railway", - "integrations/app-connections/render", - "integrations/app-connections/supabase", - "integrations/app-connections/teamcity", - "integrations/app-connections/terraform-cloud", - "integrations/app-connections/vercel", - "integrations/app-connections/windmill", - "integrations/app-connections/zabbix" - ] - } - ] - }, - { - "group": "Secret Syncs", - "pages": [ - "integrations/secret-syncs/overview", - { - "group": "Syncs", - "pages": [ - "integrations/secret-syncs/1password", - "integrations/secret-syncs/aws-parameter-store", - "integrations/secret-syncs/aws-secrets-manager", - "integrations/secret-syncs/azure-app-configuration", - "integrations/secret-syncs/azure-devops", - "integrations/secret-syncs/azure-key-vault", - "integrations/secret-syncs/bitbucket", - "integrations/secret-syncs/camunda", - "integrations/secret-syncs/checkly", - "integrations/secret-syncs/cloudflare-pages", - "integrations/secret-syncs/cloudflare-workers", - "integrations/secret-syncs/databricks", - "integrations/secret-syncs/digital-ocean-app-platform", - "integrations/secret-syncs/flyio", - "integrations/secret-syncs/gcp-secret-manager", - "integrations/secret-syncs/github", - "integrations/secret-syncs/gitlab", - "integrations/secret-syncs/hashicorp-vault", - "integrations/secret-syncs/heroku", - "integrations/secret-syncs/humanitec", - "integrations/secret-syncs/netlify", - "integrations/secret-syncs/oci-vault", - "integrations/secret-syncs/railway", - "integrations/secret-syncs/render", - "integrations/secret-syncs/supabase", - "integrations/secret-syncs/teamcity", - "integrations/secret-syncs/terraform-cloud", - "integrations/secret-syncs/vercel", - "integrations/secret-syncs/windmill", - "integrations/secret-syncs/zabbix" - ] - } - ] - }, - { - "group": "Native Integrations", - "pages": [ - { - "group": "AWS", - "pages": [ - "integrations/cloud/aws-parameter-store", - "integrations/cloud/aws-secret-manager", - "integrations/cloud/aws-amplify" + { + "group": "AWS", + "pages": [ + "integrations/cloud/aws-parameter-store", + "integrations/cloud/aws-secret-manager", + "integrations/cloud/aws-amplify" + ] + }, + "integrations/cloud/vercel", + "integrations/cloud/azure-key-vault", + "integrations/cloud/azure-app-configuration", + "integrations/cloud/azure-devops", + "integrations/cloud/gcp-secret-manager", + { + "group": "Cloudflare", + "pages": [ + "integrations/cloud/cloudflare-pages", + "integrations/cloud/cloudflare-workers" + ] + }, + "integrations/cloud/terraform-cloud", + "integrations/cloud/databricks", + { + "group": "View more", + "pages": [ + "integrations/cloud/digital-ocean-app-platform", + "integrations/cloud/heroku", + "integrations/cloud/netlify", + "integrations/cloud/flyio", + "integrations/cloud/railway", + "integrations/cloud/render", + "integrations/cloud/laravel-forge", + "integrations/cloud/supabase", + "integrations/cloud/northflank", + "integrations/cloud/hasura-cloud", + "integrations/cloud/qovery", + "integrations/cloud/hashicorp-vault", + "integrations/cloud/cloud-66", + "integrations/cloud/windmill" + ] + } ] }, - "integrations/cloud/vercel", - "integrations/cloud/azure-key-vault", - "integrations/cloud/azure-app-configuration", - "integrations/cloud/azure-devops", - "integrations/cloud/gcp-secret-manager", { - "group": "Cloudflare", + "group": "CI/CD Integrations", "pages": [ - "integrations/cloud/cloudflare-pages", - "integrations/cloud/cloudflare-workers" + "integrations/cicd/jenkins", + "integrations/cicd/githubactions", + "integrations/cicd/gitlab", + "integrations/cicd/bitbucket", + "integrations/cloud/teamcity", + { + "group": "View more", + "pages": [ + "integrations/cicd/circleci", + "integrations/cicd/travisci", + "integrations/cicd/rundeck", + "integrations/cicd/codefresh", + "integrations/cloud/checkly", + "integrations/cicd/octopus-deploy" + ] + } ] }, - "integrations/cloud/terraform-cloud", - "integrations/cloud/databricks", { - "group": "View more", + "group": "Framework Integrations", "pages": [ - "integrations/cloud/digital-ocean-app-platform", - "integrations/cloud/heroku", - "integrations/cloud/netlify", - "integrations/cloud/flyio", - "integrations/cloud/railway", - "integrations/cloud/render", - "integrations/cloud/laravel-forge", - "integrations/cloud/supabase", - "integrations/cloud/northflank", - "integrations/cloud/hasura-cloud", - "integrations/cloud/qovery", - "integrations/cloud/hashicorp-vault", - "integrations/cloud/cloud-66", - "integrations/cloud/windmill" + "integrations/frameworks/spring-boot-maven", + "integrations/frameworks/react", + "integrations/frameworks/vue", + "integrations/frameworks/express", + { + "group": "View more", + "pages": [ + "integrations/frameworks/nextjs", + "integrations/frameworks/nestjs", + "integrations/frameworks/sveltekit", + "integrations/frameworks/nuxt", + "integrations/frameworks/gatsby", + "integrations/frameworks/remix", + "integrations/frameworks/vite", + "integrations/frameworks/fiber", + "integrations/frameworks/django", + "integrations/frameworks/flask", + "integrations/frameworks/laravel", + "integrations/frameworks/rails", + "integrations/frameworks/dotnet", + "integrations/platforms/pm2", + "integrations/frameworks/ab-initio" + ] + } + ] + }, + { + "group": "Build Tool Integrations", + "pages": ["integrations/build-tools/gradle"] + }, + { + "group": "Others", + "pages": ["integrations/external/backstage"] + } + ] + }, + { + "item": "Secrets Scanning", + "groups": [ + { + "group": "Secret Scanning", + "pages": [ + "documentation/platform/secret-scanning/overview" + ] + }, + { + "group": "Datasources", + "pages": [ + "documentation/platform/secret-scanning/bitbucket", + "documentation/platform/secret-scanning/github", + "documentation/platform/secret-scanning/gitlab" ] } ] }, { - "group": "CI/CD Integrations", - "pages": [ - "integrations/cicd/jenkins", - "integrations/cicd/githubactions", - "integrations/cicd/gitlab", - "integrations/cicd/bitbucket", - "integrations/cloud/teamcity", + "item": "Infisical PKI", + "groups": [ { - "group": "View more", + "group": "Infisical PKI", "pages": [ - "integrations/cicd/circleci", - "integrations/cicd/travisci", - "integrations/cicd/rundeck", - "integrations/cicd/codefresh", - "integrations/cloud/checkly", - "integrations/cicd/octopus-deploy" + "documentation/platform/pki/overview", + "documentation/platform/pki/private-ca", + "documentation/platform/pki/external-ca", + "documentation/platform/pki/subscribers", + "documentation/platform/pki/certificates", + "documentation/platform/pki/acme-ca", + "documentation/platform/pki/est", + "documentation/platform/pki/alerting", + { + "group": "Integrations", + "pages": [ + "documentation/platform/pki/pki-issuer", + "documentation/platform/pki/integration-guides/gloo-mesh" + ] + } ] } ] }, { - "group": "Framework Integrations", - "pages": [ - "integrations/frameworks/spring-boot-maven", - "integrations/frameworks/react", - "integrations/frameworks/vue", - "integrations/frameworks/express", + "item": "Infisical SSH", + "groups": [ { - "group": "View more", + "group": "Infisical SSH", "pages": [ - "integrations/frameworks/nextjs", - "integrations/frameworks/nestjs", - "integrations/frameworks/sveltekit", - "integrations/frameworks/nuxt", - "integrations/frameworks/gatsby", - "integrations/frameworks/remix", - "integrations/frameworks/vite", - "integrations/frameworks/fiber", - "integrations/frameworks/django", - "integrations/frameworks/flask", - "integrations/frameworks/laravel", - "integrations/frameworks/rails", - "integrations/frameworks/dotnet", - "integrations/platforms/pm2", - "integrations/frameworks/ab-initio" + "documentation/platform/ssh/overview", + "documentation/platform/ssh/host-groups" ] } ] }, { - "group": "Build Tool Integrations", - "pages": ["integrations/build-tools/gradle"] - }, - { - "group": "Others", - "pages": ["integrations/external/backstage"] + "item": "Infisical KMS", + "groups": [ + { + "group": "Infisical KMS", + "pages": [ + "documentation/platform/kms/overview", + "documentation/platform/kms/hsm-integration", + "documentation/platform/kms/kubernetes-encryption", + "documentation/platform/kms/kmip" + ] + } + ] } ] }, { - "tab": "CLI", + "tab": "CLI Reference", "groups": [ { "group": "Command line", @@ -724,6 +766,10 @@ "group": "Admin", "pages": ["api-reference/endpoints/admin/bootstrap-instance"] }, + { + "group": "Events", + "pages": ["api-reference/endpoints/events/project-events"] + }, { "group": "Identities", "pages": [ @@ -1560,7 +1606,7 @@ "api-reference/endpoints/app-connections/mysql/delete" ] }, - { + { "group": "Netlify", "pages": [ "api-reference/endpoints/app-connections/netlify/list", diff --git a/docs/documentation/getting-started/concepts/audit-logs.mdx b/docs/documentation/getting-started/concepts/audit-logs.mdx new file mode 100644 index 000000000..e67f06f5a --- /dev/null +++ b/docs/documentation/getting-started/concepts/audit-logs.mdx @@ -0,0 +1,40 @@ +--- +title: "Audit Logs" +sidebarTitle: "Audit Logs" +description: "Understand how Infisical logs activity and supports external audit streaming." +--- + +Infisical records a detailed audit trail of actions across the platform — providing deep visibility into access, changes, and usage for security and compliance purposes. + +Every interaction with Infisical resources generates an audit event. These events are immutable and include metadata such as the actor, event type, affected resources, timestamp, IP address, and client source. + +Audit logs enable teams to: + +- Monitor access and changes to secrets, certificates, and infrastructure. +- Investigate incidents with full context around who did what, when, and how. +- Meet compliance and governance requirements with structured activity records. + +To learn more, refer to the [audit logs documentation](/documentation/platform/audit-logs). + +## Log Coverage + +Infisical tracks dozens of event types across the platform — including secret access, permission changes, certificate issuance, SSH session activity, and identity management. + +Each audit entry includes structured fields that make it easy to search, filter, and correlate across systems. For example: + +- Event Type: Action that occurred (e.g., `create-secret`, `issue-ssh-cert`). +- Actor: Who performed the action (user or machine identity). +- Resource: What was affected (e.g., project, secret, certificate). +- Context: IP address, user agent, permissions, and more. + +## External Log Streaming + +For centralized monitoring and long-term retention, Infisical supports [audit log streaming](/documentation/platform/audit-log-streams/audit-log-streams) to external systems. + +You can forward logs to SIEM platforms, storage buckets, or observability stacks using JSON-based collectors. Infisical integrates well with tools like [Fluent Bit](/documentation/platform/audit-log-streams/audit-log-streams-with-fluentbit#deploy-fluent-bit), enabling teams to route logs to destinations such as: + +- AWS S3 +- Elasticsearch +- Splunk +- Datadog +- Cloud-native log pipelines diff --git a/docs/documentation/getting-started/concepts/client-integrations.mdx b/docs/documentation/getting-started/concepts/client-integrations.mdx new file mode 100644 index 000000000..bcd935830 --- /dev/null +++ b/docs/documentation/getting-started/concepts/client-integrations.mdx @@ -0,0 +1,31 @@ +--- +title: "Client Ecosystem" +sidebarTitle: "Client Ecosystem" +description: "Get an overview of the CLI, SDKs, agents, APIs, and integrations that interact with Infisical." +--- + +Infisical provides a flexible interface for integrating into development workflows and infrastructure. Around it is a rich ecosystem of clients and integrations that allow users and systems to interact with Infisical across any environment. + +These clients enable access to secrets, certificates, and other resources from wherever they’re needed—whether that’s a developer’s terminal, a CI/CD pipeline, or a running Kubernetes workload. + +## Available Clients and Interfaces + +Infisical offers a non-exhaustive set of clients and interfaces to support a wide range of use cases: + +- [CLI](/cli/overview): A powerful command-line interface for developers and operators to interact with Infisical from local or automated environments. Commonly used for secret access, SSH credential issuance, and more. + +- [SDKs](/sdks/overview): Official client libraries for languages like Go, Node.js, and Python make it easy to integrate Infisical directly into applications and internal tooling. + +- [HTTP API](/api-reference/overview/introduction): A fully documented RESTful API powers all core functionality and enables advanced or custom integrations. + +- [Agents](/integrations/platforms/infisical-agent): Lightweight background processes that can fetch and sync secrets or credentials into local environments, containers, or file systems. + +- [Kubernetes Operator](/integrations/platforms/kubernetes/overview): A native controller that syncs Infisical secrets into Kubernetes as native Secrets, and supports secure workload integration. + +- [External Secrets Operator (ESO)](https://external-secrets.io/latest/provider/infisical): Allows Infisical to act as a backend provider for syncing secrets into Kubernetes `Secret` objects using the widely adopted External Secrets Operator. + +- [Kubernetes PKI Issuer](/documentation/platform/pki/pki-issuer): A controller that issues X.509 certificates from Infisical PKI using the cert-manager Issuer and Certificate CRDs. + +- [Secret Syncs](/integrations/secret-syncs/overview): Native integrations to forward secrets to services like GitHub, GitLab, AWS Secrets Manager, Vercel, and more. + +This modular ecosystem lets teams use Infisical alongside their existing stack—without requiring opinionated workflows or lock-in. diff --git a/docs/documentation/getting-started/concepts/deployment-models.mdx b/docs/documentation/getting-started/concepts/deployment-models.mdx new file mode 100644 index 000000000..39078a142 --- /dev/null +++ b/docs/documentation/getting-started/concepts/deployment-models.mdx @@ -0,0 +1,52 @@ +--- +title: "Using Infisical: Cloud or Self-Hosted" +sidebarTitle: "Cloud vs. Self-Host" +description: "Choose between Infisical Cloud or a self-managed deployment" +--- + +Infisical can be used in two ways: via [Infisical Cloud](https://app.infisical.com), a managed offering, or through a self-hosted deployment within your own infrastructure. + +Both options provide the same core platform capabilities. The decision depends on your operational model, trust boundaries, and compliance requirements. While Infisical Cloud comes with built-in security and operational guarantees, a self-hosted deployment gives you full control—but also full responsibility for securing and maintaining the system. + +## Infisical Cloud + +Infisical Cloud is our managed service found at [app.infisical.com](https://app.infisical.com). It includes automated updates, availability guarantees, and secure infrastructure operations. + +For most teams, Infisical Cloud is the recommended way to get started. It simplifies adoption by removing the need to manage deployment, scaling, or maintenance internally. + +Use this if: + +- You prefer not to operate infrastructure or handle upgrades +- You require a secure, production-grade hosted service +- You want to adopt Infisical with minimal operational overhead + + +

+ By default, Infisical Cloud is a secure, multi-tenant service. For + enterprises with stricter isolation or regulatory needs, dedicated cloud + instances are available. +

+

Contact sales@infisical.com to learn more.

+
+ +## Self-Hosted Infisical + +Infisical can also be deployed and managed within your own infrastructure. This approach provides full control over platform configuration, data storage, and operational security. In this model, your team is responsible for maintaining uptime, monitoring, patching, and integrations. + +Use this if: + +- You require complete control over data, deployment, and security posture +- Your compliance model mandates self-managed or on-premise systems +- You need to tightly integrate with internal tooling and infrastructure + +Infisical supports multiple deployment methods, including [Docker](/self-hosting/deployment-options/standalone-infisical), [Docker Compose](/self-hosting/deployment-options/docker-compose), [Kubernetes](/self-hosting/deployment-options/kubernetes-helm), and [Linux package](/self-hosting/deployment-options/native/linux-package/installation). + +To learn more, refer to the [self-hosting documentation](/self-hosting/overview). + + +

+ The open-source core is available under the MIT license. Additional + enterprise features and support are available with a commercial license. +

+

Contact sales@infisical.com to learn more.

+
diff --git a/docs/documentation/getting-started/concepts/platform-hierarchy.mdx b/docs/documentation/getting-started/concepts/platform-hierarchy.mdx new file mode 100644 index 000000000..7697f935b --- /dev/null +++ b/docs/documentation/getting-started/concepts/platform-hierarchy.mdx @@ -0,0 +1,41 @@ +--- +title: "Platform Hierarchy" +sidebarTitle: "Platform Hierarchy" +description: "Understand how organizations and projects are structured in Infisical." +--- + +Infisical is structured around organizations and projects, allowing teams to manage multiple products, access scopes, and use cases within a single account while keeping boundaries and responsibilities clearly defined. + +## Organizations + +An [organization](/documentation/platform/organization) typically represents a company or high-level entity (e.g. Acme Corp). It acts as the umbrella for all projects, members, and billing settings. + +[Users](/documentation/platform/identities/user-identities) are invited to an organization and assigned [organization-level roles](/documentation/platform/access-controls/role-based-access-controls#organization-level-access-controls) that determine what they can manage—such as members, machine identities, and billing details. + +![organization](/images/platform/organization/organization.png) + +## Projects + +A [project](/documentation/platform/project) belongs to an organization and defines a specific scope of work. Each project has a product type such as Secrets Management, SSH, or PKI that determines what features are available in that project. + +For example: + +- A Secrets Management project manages application secrets across environments. + +- An SSH project enables certificate-based access to infrastructure. + +- A PKI project manages certificate authorities and X.509 certificate workflows. + +Users are added to a project and assigned [project-level roles](/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls) that determine what they can manage—such as secrets, access policies, or certificate authorities. A user can have different roles across projects, allowing for flexible and fine-grained access control that reflects how teams operate in practice. + +![organization projects](/images/platform/organization/organization-projects.png) + +## Key Characteristics + +- Projects are isolated in terms of configuration, permissions, and product workflows. + +- Access is managed independently at both the organization and project level. + +- All projects within an organization share the same billing and user directory. + +Teams can adopt Infisical incrementally—starting with one product and expanding as needed. diff --git a/docs/documentation/getting-started/concepts/platform-iam.mdx b/docs/documentation/getting-started/concepts/platform-iam.mdx new file mode 100644 index 000000000..87c343269 --- /dev/null +++ b/docs/documentation/getting-started/concepts/platform-iam.mdx @@ -0,0 +1,29 @@ +--- +title: "Platform Identity and Access Management" +sidebarTitle: "Platform IAM" +description: "Understand how users, machine identities, roles, and permissions are managed." +--- + +Infisical uses identity-based access control to govern how users and systems interact with secrets, certificates, infrastructure, and other resources on the platform. + +There are two types of identities: + +- [User identities](/documentation/platform/identities/user-identities): Represent individuals such as developers or administrators that typically access the platform via browser. + +- [Machine identities](/documentation/platform/identities/machine-identities): Represent systems such as CI pipelines or applications that programmatically interact with the platform. + +Each identity is granted access based on its assigned roles and permissions and must authenticate with the platform in order to access any resources. + +To learn more, refer to the [identities documentation](/documentation/platform/identities/overview). + +## Roles and Access + +Infisical provides a robust and flexible access control system. The primary authorization mechanism is [role-based access control (RBAC)](/documentation/platform/access-controls/role-based-access-controls), where identities are assigned roles at two access control levels: + +- [Organization-level access control](/documentation/platform/access-controls/role-based-access-controls#organization-level-access-controls): Control billing, member management, and platform-wide settings + +- [Project-level access control](/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls): Control access to specific product resources like secrets, SSH hosts, or certificates + +Beyond RBAC, Infisical also supports additional project-level permissioning features, [including attribute-based access control (ABAC)](/documentation/platform/access-controls/abac/overview), [temporary access grants](/documentation/platform/access-controls/temporary-access), and [additional privileges](/documentation/platform/access-controls/additional-privileges) for select project types. + +To learn more, refer to the [access control documentation](/documentation/platform/access-controls/overview). diff --git a/docs/documentation/getting-started/introduction-new.mdx b/docs/documentation/getting-started/introduction-new.mdx deleted file mode 100644 index c8eee8739..000000000 --- a/docs/documentation/getting-started/introduction-new.mdx +++ /dev/null @@ -1,107 +0,0 @@ ---- -mode: 'custom' ---- - -export function openSearch() { - document.getElementById('search-bar-entry').click(); -} - -
-
- -
-
-
- Infisical Documentation -
-

- What can we help you build? -

- -
-
- -
- -
- Choose a topic below or simply{' '} - get started -
- - - - Practical guides and best practices to get you up and running quickly. - - - Comprehensive details about the Infisical API. - - - Learn more about Infisical's architecture and underlying security. - - - Read self-hosting instruction for Infisical. - - - Infisical's growing number of third-party integrations. - - - News about features and changes in Pinecone and related tools. - - - -
\ No newline at end of file diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index d73c28ab5..f773019ec 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -1,106 +1,40 @@ --- title: "What is Infisical?" sidebarTitle: "What is Infisical?" -description: "An Introduction to the Infisical secret management platform." +description: "The open source platform for managing secrets, certificates, and secure infrastructure access." --- -**[Infisical](https://infisical.com)** is the open source secret management platform that developers use to centralize their application configuration and secrets like API keys and database credentials as well as manage their internal PKI. Additionally, developers use Infisical to prevent secrets leaks to git and securely share secrets amongst engineers. +## What is Infisical? + +[Infisical](https://infisical.com) is the [open source](https://github.com/Infisical/infisical), all-in-one platform for secrets, certificates, and privileged access management. + +It provides modern security workflows — including secrets rotation, dynamic credentials, access approvals, and SSH certificate-based access — all within one platform designed for developers, infrastructure, and security teams. Start managing secrets securely with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself. - - - Get started with Infisical Cloud in just a few minutes. - - - Self-host Infisical on your own infrastructure. - - +## Why use Infisical? -## Why Infisical? +Managing secrets, credentials, and infrastructure access is a critical concern for engineering teams. As infrastructure scales and environments become more complex, [secrets start to sprawl](https://infisical.com/blog/what-is-secret-sprawl) — across codebases, CI/CD pipelines, configuration files, and cloud services. This makes them difficult to track, rotate, and secure. -Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical: +Without proper management, secret sprawl turns into risk: hardcoded credentials, unrotated keys, fragmented access controls that attackers can exploit amongst other things. -- Streamlined **local development** processes (switching .env files to [Infisical CLI](/cli/commands/run) and removing secrets from developer machines). -- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project). -- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments. -- Secure and compliant secret management practices in **[production environments](/sdks/overview)**. -- **Facilitated workflows** around [secret change management](/documentation/platform/pr-workflows), [access requests](/documentation/platform/access-controls/access-requests), [temporary access provisioning](/documentation/platform/access-controls/temporary-access), and more. -- **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities. +Infisical addresses this challenge by providing an all-in-one platform and workflows to: -## How does Infisical work? +- Securely store and manage application secrets from development to production. +- Scan code and pipelines for exposed credentials. +- Automate X.509 certificate issuance and renewal. +- Manage SSH access using short-lived, policy-driven certificates. +- Encrypt and decrypt sensitive data with centralized key control. +- Audit every access, credential use, and change. -To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below. +Infisical is designed to integrate cleanly into your stack—improving security without adding complexity. -**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**. +## What does Infisical include? -As a result, the 3 main concepts that are important to understand are: +Infisical consists of several tightly integrated products, each designed to solve a specific part of the infrastructure security surface: -- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them. -- **[Clients](/integrations/platforms/kubernetes)**: Infisical-developed tools for managing secrets in various infrastructure components (e.g., [Kubernetes Operator](/integrations/platforms/kubernetes), [Infisical Agent](/integrations/platforms/infisical-agent), [CLI](/cli/usage), [SDKs](/sdks/overview), [API](/api-reference/overview/introduction), [Web Dashboard](/documentation/platform/organization)). -- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, AWS Auth etc.). - -## How to get started with Infisical? - -Depending on your use case, it might be helpful to look into some of the resources and guides provided below. - - - - Inject secrets into any application process/environment. - - - Fetch secrets with any programming language on demand. - - - Inject secrets into Docker containers. - - - Fetch and save secrets as native Kubernetes secrets. - - - Fetch secrets via HTTP request. - - - Explore integrations for GitHub, Vercel, AWS, and more. - - +- [Secrets Management](/documentation/platform/secrets-mgmt/overview): Securely store, access, and distribute secrets across environments with fine-grained controls, automatic rotation, and audit logging. +- [Secrets Scanning](/documentation/platform/secret-scanning/overview): Detect hardcoded secrets in code, CI pipelines, and infrastructure—integrated with GitHub, GitLab, Bitbucket, and more. +- [Infisical PKI](/documentation/platform/pki/overview): Issue and manage X.509 certificates using protocols like EST, with support for internal and external CAs. +- [Infisical SSH](/documentation/platform/ssh/overview): Provide short-lived SSH access to servers using certificate-based authentication, replacing static keys with policy-driven, time-bound control. +- [Infisical KMS](/documentation/platform/kms/overview): Encrypt and decrypt data using centrally managed keys with enforced access policies and full audit visibility. diff --git a/docs/documentation/getting-started/overview.mdx b/docs/documentation/getting-started/overview.mdx new file mode 100644 index 000000000..769990987 --- /dev/null +++ b/docs/documentation/getting-started/overview.mdx @@ -0,0 +1,77 @@ +--- +title: "Overview" +sidebarTitle: "Overview" +description: "The open source platform for managing secrets, certificates, and secure infrastructure access." +--- + + + Learn what Infisical is and how it can help you manage secrets, certificates, + and secure access across your infrastructure. + + +## Products + + + + Securely store, manage, and control access to sensitive application secrets across your environments. + + + + Automatically detect and alert on hardcoded secrets in source code, CI pipelines, and infrastructure. + + + Automate the issuance and management of X.509 certificates across your infrastructure using modern protocols like EST. + + + Replace static SSH keys with short-lived SSH certificates to simplify access and improve security. + + + + + + Encrypt and decrypt sensitive data using a centralized key management system. + + + +## Resources + + + + Explore Infisical’s command-line interface for managing secrets, + certificates, and system operations via terminal. + + + Browse Infisical’s API documentation to programmatically interact with + secrets, access controls, and certificate workflows. + + + + + Learn how to deploy and operate Infisical on your own infrastructure with full + control and data ownership. + + diff --git a/docs/documentation/platform/event-subscriptions.mdx b/docs/documentation/platform/event-subscriptions.mdx new file mode 100644 index 000000000..ca6ceebd1 --- /dev/null +++ b/docs/documentation/platform/event-subscriptions.mdx @@ -0,0 +1,118 @@ +--- +title: "Event Subscriptions" +sidebarTitle: "Events" +description: "Subscribe to events in Infisical for real-time updates" +--- + + + **Note:** Event Subscriptions is a paid feature. - **Infisical Cloud users:** Event Subscriptions is available under + the **Enterprise Tier**. - **Self-Hosted Infisical:** Please contact [sales@infisical.com](mailto:sales@infisical.com) + to purchase an enterprise license. + + +Event Subscriptions in Infisical allow you to receive real-time notifications when specific actions occur within your account or organization. These notifications include changes to secrets, users, teams, and many more **coming soon**. + +## How It Works + +- Server receives message over pubsub connection indicating changes have occurred +- Server processes the change notification +- Updated data is synchronized across all connected Infisical instances +- Client applications receive real-time updates through [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) +- All servers maintain consistent state without manual intervention + +This ensures your infrastructure stays up-to-date automatically, without requiring restarts or manual synchronization. + + + Event Subscriptions are designed for real-time communication and do not include persistence or replay + capabilities—events are delivered once and are not stored for future retrieval. + + +## Supported Resources + +You can currently subscribe to notifications for the following resources and event types: + +- **Secrets** + - `secret:created`: Triggered when a secret is created + - `secret:updated`: Triggered when a secret is updated + - `secret:deleted`: Triggered when a secret is deleted + +## Permissions Setup + +To receive events on a supported resource, the identity must have `Subscribe` action permission on that resource. + +Follow these steps to set up the necessary permissions: + + + + ![Select Project](/images/platform/events/select-project.png) + +On your project page, open **Project Settings** from the sidebar. + +In the Project name section, click **Copy Project ID** to copy your Project ID, or extract it from the URL: +`https://app.infisical.com/project//settings` + + + + + ![Project Detail](/images/platform/events/project-detail.png) ![Project + Access](/images/platform/events/project-access.png) Navigate to **Access Management**, then select **Project Roles**. + + + + ![Project Role](/images/platform/events/project-role.png) You can either edit an existing role or create a new role + for event subscriptions. + + + + ![Role Detail](/images/platform/events/role-detail.png) Select the specific resources that the role should have access + to. ![Add policy](/images/platform/events/add-policy.png) + + + + ![Policy setting](/images/platform/events/policy-setting.png) + + Ensure the **Subscribe** action is selected for the relevant resources and events. + + ## Conditions + + By default, the role will have access to all events for the selected resources in this project. + + + + ![Policy setting](/images/platform/events/access-full.png) + + + ![Policy setting](/images/platform/events/access-path.png) + + + ![Policy setting](/images/platform/events/access-dev.png) + + + + + +## Getting Started + +Currently, events are only available via [API](/api-reference/endpoints/events) but will soon be available in our SDKs, Kubernetes Operator, and more. + +### API Usage + +You need an auth token to use this API. To get an authentication token, follow the authentication guide for one of our supported auth methods from the [machine identities documentation](/documentation/platform/identities/machine-identities#authentication-methods). + +#### Creating a Subscription + +![Postman Subscription](/images/platform/events/postman-subscribe.png) + +**Request Parameters:** + +- `projectId`: Project whose events you want to subscribe to +- `register`: List of event filters + - `conditions`: Conditions to filter events on + - `environmentSlug`: Project environment + - `secretPath`: Path of the secrets + +![Postman Subscription Response](/images/platform/events/postman-sse-response.png) + +The subscribe endpoint responds with a `text/event-stream` content type to initiate SSE streaming. + +For more specific details, please refer to our [API Reference](/api-reference/endpoints/events). diff --git a/docs/documentation/platform/identities/auth-templates.mdx b/docs/documentation/platform/identities/auth-templates.mdx new file mode 100644 index 000000000..5c389376f --- /dev/null +++ b/docs/documentation/platform/identities/auth-templates.mdx @@ -0,0 +1,96 @@ +--- +title: "Machine Identity Auth Templates" +description: "Learn how to use auth templates to standardize authentication configurations for machine identities." +--- + +## Concept + +Machine Identity Auth Templates allow you to create reusable authentication configurations that can be applied across multiple machine identities. This feature helps standardize authentication setups, reduces configuration drift, and simplifies identity management at scale. + +Instead of manually configuring authentication settings for each identity, you can create templates with predefined authentication parameters and apply them to multiple identities. This ensures consistency and reduces the likelihood of configuration errors. + +Key Benefits: + +- **Standardization**: Ensure consistent authentication configurations across identities +- **Efficiency**: Reduce time spent configuring individual identities +- **Governance**: Centrally manage and update authentication parameters +- **Scalability**: Easily apply proven configurations to new identities + +## Managing Auth Templates + +Auth templates are managed in **Organization Settings > Access Control > Identities** under the **Identity Auth Templates** section. + +![Identity Auth Templates Section](/images/platform/identities/auth-templates/templates-section.png) + +### Creating a Template + + + + In your organization settings, go to **Access Control > Identities** and scroll down to the **Identity Auth Templates** section. + + + + Click **Create Template** to open the template creation modal. + + ![Create Template Button](/images/platform/identities/auth-templates/create-template-button.png) + + Select the authentication method you want to create a template for (currently supports LDAP Auth). + + + + Fill in the template configuration based on your chosen authentication method. + + + + **For LDAP Auth templates**, configure the following fields: + + ![LDAP Auth Template](/images/platform/identities/auth-templates/ldap-template.png) + + - **Template Name**: A descriptive name for your template + - **URL**: The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` _(for connection over SSL/TLS)_, etc. + - **Bind DN**: The DN to bind to the LDAP server with. + - **Bind Pass**: The password to bind to the LDAP server with. + - **Search Base / DN**: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`. + - **CA Certificate**: The CA certificate to use when verifying the LDAP server certificate. This field is optional but recommended. + + + You can read more about LDAP Auth configuration in the [LDAP Auth documentation](/documentation/platform/identities/ldap-auth/general). + + + + + + +### Using Templates + +Once created, templates can be applied when configuring authentication methods for machine identities. When adding an auth method to an identity, you'll have the option to select from available templates or configure manually. + +![Attach Template](/images/platform/identities/auth-templates/machine-identity-page.png) +![Attach Template Form](/images/platform/identities/auth-templates/attach-template-form.png) + +### Managing Template Usage + +You can view which identities are using a specific template by clicking **View Usages** in the template's dropdown menu. + +![Template Usages](/images/platform/identities/auth-templates/template-usages.png) +![Template Usages Modal](/images/platform/identities/auth-templates/template-usages-modal.png) + +## FAQ + + + + Yes, you can edit existing templates. After editing a template, changes to templates will automatically update identities that are already using them. + + + + If you delete a template that's currently being used by identities, those identities will continue to function with their existing configuration. However, the link to the template will be broken, and you won't be able to use the template for new identities. + + + + Yes, click **View Usages** in the template's dropdown menu to see all identities currently using that template. + + + + Currently, auth templates support LDAP Auth. Support for additional authentication methods will be added in future releases. + + \ No newline at end of file diff --git a/docs/documentation/platform/identities/ldap-auth/general.mdx b/docs/documentation/platform/identities/ldap-auth/general.mdx index 7fb2798c7..01395b68c 100644 --- a/docs/documentation/platform/identities/ldap-auth/general.mdx +++ b/docs/documentation/platform/identities/ldap-auth/general.mdx @@ -5,6 +5,12 @@ description: "Learn how to authenticate with Infisical using LDAP." **LDAP Auth** is an LDAP based authentication method that allows you to authenticate with Infisical using a machine identity configured with an [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) directory. +## Templates + +You can create reusable LDAP authentication templates to standardize configurations across multiple machine identities. Templates help ensure consistency, reduce configuration errors, and simplify identity management at scale. + +To create and manage LDAP auth templates, see our [Machine Identity Auth Templates documentation](/documentation/platform/identities/auth-templates). Once you've created a template, you can apply it when configuring LDAP auth for your identities in the guide below. + ## Guide diff --git a/docs/documentation/platform/identities/user-identities.mdx b/docs/documentation/platform/identities/user-identities.mdx index bcb470a3e..31e4bf242 100644 --- a/docs/documentation/platform/identities/user-identities.mdx +++ b/docs/documentation/platform/identities/user-identities.mdx @@ -5,18 +5,19 @@ description: "Read more about the concept of user identities in Infisical." ## Concept -A **user identity** (also known as **user**) represents a developer, admin, or any other human entity interacting with resources in Infisical. +A **user identity** (also known as **user**) represents a developer, admin, or any other human entity interacting with resources in Infisical. -Users can be added manually (through Web UI) or programmatically (e.g., API) to [organizations](../organization) and [projects](../projects). +Users can be added manually (through Web UI) or programmatically (e.g., API) to [organizations](../organization) and [projects](../projects). -Upon being added to an organization and projects, users assume a certain set of roles and permissions that represents their identity. +Upon being added to an organization and projects, users assume a certain set of roles and permissions that represents their identity. -![organization members](../../../images/platform/organization/organization-members.png) +![organization users](/images/platform/organization/organization-users.png) ## Authentication methods -To interact with various resources in Infisical, users are able to utilize a number of authentication methods: -- **Email & Password**: the most common authentication method that is used for authentication into Web Dashboard and Infisical CLI. It is recommended to utilize [Multi-factor Authentication](/documentation/platform/mfa) in addition to it. -- **SSO**: Infisical natively integrates with a number of SSO identity providers like [Google](/documentation/platform/sso/google), [GitHub](/documentation/platform/sso/github), and [GitLab](/documentation/platform/sso/gitlab). -- **SAML SSO**: It is also possible to set up SAML SSO integration with identity providers like [Okta](/documentation/platform/sso/okta), [Microsoft Entra ID](/documentation/platform/sso/azure) (formerly known as Azure AD), [JumpCloud](/documentation/platform/sso/jumpcloud), [Google](/documentation/platform/sso/google-saml), and more. +To interact with various resources in Infisical, users are able to utilize a number of authentication methods: + +- **Email & Password**: the most common authentication method that is used for authentication into Web Dashboard and Infisical CLI. It is recommended to utilize [Multi-factor Authentication](/documentation/platform/mfa) in addition to it. +- **SSO**: Infisical natively integrates with a number of SSO identity providers like [Google](/documentation/platform/sso/google), [GitHub](/documentation/platform/sso/github), and [GitLab](/documentation/platform/sso/gitlab). +- **SAML SSO**: It is also possible to set up SAML SSO integration with identity providers like [Okta](/documentation/platform/sso/okta), [Microsoft Entra ID](/documentation/platform/sso/azure) (formerly known as Azure AD), [JumpCloud](/documentation/platform/sso/jumpcloud), [Google](/documentation/platform/sso/google-saml), and more. - **LDAP**: For organizations with more advanced needs, Infisical also provides user authentication with [LDAP](/documentation/platform/ldap/overview) that includes a number of LDAP providers. diff --git a/docs/documentation/platform/organization.mdx b/docs/documentation/platform/organization.mdx index 6c3b218ab..aea842d3f 100644 --- a/docs/documentation/platform/organization.mdx +++ b/docs/documentation/platform/organization.mdx @@ -3,74 +3,94 @@ title: "Organizations" description: "Learn more and understand the concept of Infisical organizations." --- -An Infisical organization is a set of [projects](./project) that use the same billing. Organizations allow one or more users to control billing and project permissions for all of the projects belonging to the organization. Each project belongs to an organization. +Infisical is structured around organizations and [projects](/documentation/platform/project). + +## Organizations + +An organization represents a company or high-level entity (e.g. Acme Corp) and acts as the root scope for managing members and machine identities, projects, usage and billing, global integrations and configuration (such as single sign-on, provisioning, etc), and more. + +Within an organization, you can create any number of projects—each tied to a specific product type such as Secrets Management or PKI that determines the functionality available. + +![organization](/images/platform/organization/organization.png) ## Projects -The **Projects** page is where you can view the projects that you have access to within your organization -as well as create a new project. +The _Projects_ tab shows a list of projects that you have access to. -![organization](../../images/platform/organization/organization-projects.png) +If you're an organization admin, you also have the option to view _All Projects_—a complete view of every project within the organization, including those you are not currently a member of— and gain access to any project. -## Settings +Admins can gain access to any project in the organization by opening the options menu (⋮) next to a project and selecting Access. This will add you to the project as an admin and allow full visibility and control. -The **Settings** page lets you manage information about your organization including: +![organization projects](/images/platform/organization/organization-projects.png) -- **Name**: The name of your organization. -- **Slug**: The slug of your organization. -- **Default Organization Member Role**: The role assigned to users when joining your organization unless otherwise specified. -- **Incident Contacts**: Emails that should be alerted if anything abnormal is detected within the organization. -- **Enabled Products**: Products which are enabled for your organization. This setting strictly affects the sidebar UI; disabling a product does not disable its API or routes. +## Roles and Access Control -![organization settings general](../../images/platform/organization/organization-settings-general.png) +The _Access Control_ tab lets you view and manage roles and permissions for users, machine identities, and groups across your organization. -- Security and Authentication: A set of setting to enforce or manage [SAML](/documentation/platform/sso/overview), [OIDC](/documentation/platform/sso/overview), [SCIM](/documentation/platform/scim/overview), [LDAP](/documentation/platform/ldap/overview), and other authentication configurations. +Users are invited to an organization and assigned organization-level roles such as `Admin` or `Member`. You can also define [custom roles](/documentation/platform/access-controls/role-based-access-controls#creating-custom-roles) at the organization level to fit your permission model. -![organization settings auth](../../images/platform/organization/organization-settings-auth.png) +![organization users](/images/platform/organization/organization-users.png) - - You can adjust the maximum time a user token will remain valid for your organization. After this period, users will be required to re-authenticate. This helps improve security by enforcing regular sign-ins. - +Infisical supports [user identities](/documentation/platform/identities/user-identities) (representing people) and [machine identities](/documentation/platform/identities/machine-identities) (representing services, CI/CD pipelines, or agents). The same roles and permissions can be applied to either type of identity. -## Access Control +To manage access at scale, Infisical also supports [user groups](/documentation/platform/groups) — roles assigned to a group apply to all of its members automatically. -The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization. -You can add or remove additional members as well as modify their permissions. +Note that Infisical distinguishes between organization-level and project-level access control: -![organization members](../../images/platform/organization/organization-members.png) -![organization identities](../../images/platform/organization/organization-machine-identities.png) +- [Organization-level access control](/documentation/platform/access-controls/role-based-access-controls#organization-level-access-controls): Roles and permissions governing access to organization-level resources and controls such as billing, member management, and identity provider configuration. +- [Project-level access control](/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls): Roles and permissions governing access to resources and workflows within a specific project (e.g., secrets, certificates, SSH hosts). -In the **Organization Roles** tab, you can edit current or create new custom roles for members within the organization. +![organization roles](/images/platform/organization/organization-roles.png) + +To learn more about how permissions work in detail, refer to the [access control documentation](/documentation/platform/access-controls/overview). - Note that Role-Based Access Management (RBAC) is partly a paid feature. - - Infisical provides immutable roles like `admin`, `member`, etc. - at the organization and project level for free. +Infisical provides immutable roles such as `admin` and `member` for free. If you're using Infisical Cloud, the ability to create custom roles is available under the **Pro Tier**. + If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. -![organization roles](../../images/platform/organization/organization-members-roles.png) - -As you can see next, Infisical supports granular permissions that you can tailor to each role. -If you need certain members to only be able to access billing details, for example, then you can -assign them that permission only. - -![organization role permissions](../../images/platform/organization/organization-members-roles-add-perm.png) - ## Usage & Billing -The **Usage & Billing** page applies only to [Infisical Cloud](https://app.infisical.com) and is where you can -manage your plan and billing information. +The _Usage & Billing_ tab provides an overview of your organization's billing information and platform usage. -This includes the following items: +Infisical calculates usage at the organization level—aggregating activity across all projects and product types (e.g., Secrets Management, SSH, PKI). From this tab, you can track usage, view billing details, and manage your Infisical Cloud subscription. -- Current plan: The current plan information such as what tier your organization is on and what features/limits apply to this tier. -- Licenses: The license keys for self-hosted instances of Infisical (if applicable). -- Receipts: The receipts of monthly/annual invoices. -- Billing: The billing details of your organization including payment methods on file, tax IDs (if applicable), etc. +![organization billing](/images/platform/organization/organization-billing.png) -![organization usage and billing](../../images/platform/organization/organization-usage-billing.png) +## Audit Logs + +Infisical provides a unified view of [audit logs](/documentation/platform/audit-logs) at the organization level. All platform activity—including secret access, certificate issuance, platform logins across the organization —is recorded and searchable in a central log view. + +Audit logs are also viewable at the project level, where they are scoped to show only events relevant to that specific project. This allows project administrators to monitor activity and investigate changes without requiring organization-wide access. + +## App Connections + +Infisical supports [app connections](/integrations/app-connections/overview) — integrations configured at the organization level with third-party platforms such as AWS, GCP, GitHub, and many others. + +Once configured, these connections can be reused across multiple projects as part of any feature that requires third-party integrations—such as [secret syncing](/integrations/secret-syncs/overview) or [dynamic credential generation](/documentation/platform/dynamic-secrets/overview). + +![organization app connections](/images/platform/organization/organization-app-connections.png) + +To learn more, refer to the [app connections documentation](/integrations/app-connections/overview). + +## Organization Settings + +The _Organization Settings_ tab lets you configure global behavior and security controls for the organization. + +Key configuration areas include: + +- General: Manage the organization’s name, slug, and default role for newly invited members. +- Single Sign-On (SSO): Enable [SAML](/documentation/platform/sso/overview), [LDAP](/documentation/platform/ldap/overview), or [OIDC-based](/documentation/platform/sso/general-oidc/overview) authentication for user login. +- Provisioning: Enable [SCIM](/documentation/platform/scim/overview) to automatically provision and deprovision users and groups from an identity provider. +- Security Policies: Enforce MFA and configure session duration limits. +- Encryption: Integrate with external KMS systems or bring your own encryption keys (BYOK). +- [Audit Log Streaming](/documentation/platform/audit-log-streams/audit-log-streams): Forward audit events to third-party logging tools like SIEMs or cloud storage. +- Workflow Integrations: Trigger [Slack](/documentation/platform/workflow-integrations/slack-integration) or [Microsoft Teams](/documentation/platform/workflow-integrations/microsoft-teams-integration) notifications for events like access requests. +- [Project Templates](/documentation/platform/project-templates): Define default environments, roles, and settings to standardize project creation. +- KMIP (Enterprise): Connect to KMIP-compatible HSMs for hardware-backed key storage and operations. + +![organization settings](/images/platform/organization/organization-settings.png) diff --git a/docs/documentation/platform/pki/overview.mdx b/docs/documentation/platform/pki/overview.mdx index 8ee9b113d..b2351813c 100644 --- a/docs/documentation/platform/pki/overview.mdx +++ b/docs/documentation/platform/pki/overview.mdx @@ -1,13 +1,13 @@ --- -title: "Internal PKI" +title: "Infisical PKI" sidebarTitle: "Overview" description: "Learn how to create a Private CA hierarchy and issue X.509 certificates." --- -Infisical can be used to create a Private Certificate Authority (CA) hierarchy and issue X.509 certificates for internal use. This allows you to manage your own PKI infrastructure and issue digital certificates for subscribers such as services, applications, and devices. +Infisical can be used to create and manage Certificate Authorities (CAs) and issue X.509 certificates. This allows you to manage PKI infrastructure and issue digital certificates for subscribers such as services, applications, and devices. Infisical's PKI offering is split into three components: -- [Certificate Authorities](/documentation/platform/pki/private-ca): Create and manage private CAs, including root and intermediate CAs. +- [Certificate Authorities](/documentation/platform/pki/private-ca): Create and manage CAs, including root and intermediate CAs. - [Subscribers](/documentation/platform/pki/subscribers): Define and manage entities that will request X.509 certificates from CAs. This module provides a centralized view of all subscribers, enabling you to issue certificates and monitor their status. - [Certificates](/documentation/platform/pki/certificates): Track and monitor issued X.509 certificates, maintaining a comprehensive inventory of all active and expired certificates. diff --git a/docs/documentation/platform/project.mdx b/docs/documentation/platform/project.mdx index dcf447169..7d0df2e22 100644 --- a/docs/documentation/platform/project.mdx +++ b/docs/documentation/platform/project.mdx @@ -1,116 +1,51 @@ --- -title: "Projects" +title: "Overview" description: "Learn more and understand the concept of Infisical projects." --- -A project in Infisical belongs to an [organization](./organization) and contains a number of environments, folders, and secrets. -Only users and machine identities who belong to a project can access resources inside of it according to predefined permissions. +## Projects -Infisical also allows users to request project access. Refer to the [project access request section](./access-controls/project-access-requests) +A project defines a specific scope of work for a given product line in Infisical. -## Project environments +Projects are created within an [organization](/documentation/platform/organization), and an organization can contain multiple projects across different product types. -For both visual and organizational structure, Infisical allows splitting up secrets into environments (e.g., development, staging, production). In project settings, such environments can be -customized depending on the intended use case. +## Project Types -![project secrets overview](../../images/platform/project/project-environments.png) +Infisical supports project types, each representing a different security product with its own dashboard, workflows, and capabilities. -## Secrets Overview +![project types](/images/platform/project/project-types.png) -The **Secrets Overview** page captures a birds-eye-view of secrets and [folders](./folder) across environments. -This is useful for comparing secrets, identifying if anything is missing, and making quick changes. +The supported project types are: -![project secrets overview](../../images/platform/project/project-secrets-overview-open.png) +- [Secrets Management](/documentation/platform/secrets-mgmt/overview): Securely store, access, and distribute secrets across environments with fine-grained controls, automatic rotation, and audit logging. +- [Secrets Scanning](/documentation/platform/secret-scanning/overview): Detect hardcoded secrets in code, CI pipelines, and infrastructure—integrated with GitHub, GitLab, Bitbucket, and more. +- [Infisical PKI](/documentation/platform/pki/overview): Issue and manage X.509 certificates using protocols like EST, with support for internal and external CAs. +- [Infisical SSH](/documentation/platform/ssh/overview): Provide short-lived SSH access to servers using certificate-based authentication, replacing static keys with policy-driven, time-bound control. +- [Infisical KMS](/documentation/platform/kms/overview): Encrypt and decrypt data using centrally managed keys with enforced access policies and full audit visibility. -## Secrets Dashboard +## Roles and Access Control -The **Secrets Dashboard** page appears when you press to manage the secrets of a specific environment. +[Users](/documentation/platform/identities/user-identities) and [machine identities](/documentation/platform/identities/machine-identities) must be added to a project to access its resources. Each identity is assigned a [project-level role](/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls) that defines what they can manage—such as secrets, certificates, or SSH access. These roles apply to both individuals and [user groups](/documentation/platform/groups), enabling scalable access across teams and environments. -![project dashboard](../../images/dashboard.png) +Project access is strictly scoped: only members of a project can view or manage its resources. If someone needs access but isn’t part of the project, they can submit an access request. -### Secrets +Each project in Infisical has its own [access control model](/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls), distinct from [organization-level access control](/documentation/platform/access-controls/role-based-access-controls#organization-level-access-controls). While organization roles govern broader administrative access, project-level roles control what users, groups, and machine identities can do within the boundaries of a specific project—such as managing secrets, issuing certificates, or configuring SSH access. -To add a secret, press **Add Secret** button at the top of the dashboard. +Depending on the project type (e.g. Secrets Management, PKI, SSH), project-level access control supports advanced features like [temporary access](/documentation/platform/access-controls/temporary-access), [access requests](/documentation/platform/access-controls/access-requests), and [additional privileges](/documentation/platform/access-controls/additional-privileges). -![project add secret](../../images/platform/project/project-secrets-add.png) +![project roles](/images/platform/project/project-roles.png) -For a new project, it can be convenient to populate the dashboard by dropping a `.env` file into the provided pane as shown below: +To learn more about how permissions work in detail, refer to the [access control documentation](/documentation/platform/access-controls/overview). -![project drop env file](../../images/platform/project/project-secrets-drop-env.png) +## Audit Logs -To delete a secret, hover over it and press the **X** button that appears on the right side. +Infisical provides [audit logging](/documentation/platform/audit-logs) at the project level to help teams monitor activity and maintain accountability within a specific project. These logs capture all relevant events—such as secret access, certificate issuance, and SSH activity—that occur within the boundaries of that project. -![project delete secret](../../images/platform/project/project-secrets-delete.png) +Unlike the organization-level audit view, which aggregates logs across all projects in one centralized interface, the project-level audit view is scoped to a single project. This enables relevant project admins and contributors to review activity relevant to their work without having broader access to audit logs in other projects that they are not part of. -To delete multiple secrets at once, hover over and select the secrets you'd like to delete -and press the **Delete** button that appears at the top. +## Project Settings -![project delete secret batch](../../images/platform/project/project-secrets-delete-batch.png) +Each project has its own settings panel, with options that vary depending on the selected product type. These may include +setup and configuration for environments, tags, behaviors, encryption strategies, and other options. -### Search - -To search for specific secrets by their key name, you can use the search bar. - -![project search](../../images/platform/project/project-secrets-search.png) - -To assist you with finding secrets, you can also group them by similar prefixes and filter them by tags (if applicable). - -![project filter](../../images/platform/project/project-secrets-filter.png) - -### Hide/Un-hide - -To view/hide all secrets at once, toggle the hide or un-hide button. - -![project filter](../../images/platform/project/project-secrets-unhide.png) - -### Download as .env - -To download/export secrets back into a `.env` file, press the download button. - -![project download back env](../../images/platform/project/project-secrets-download-env.png) - -### Tags - -To better organize similar secrets, hover over them and label them with a tag. - -![project tag secret](../../images/platform/project/project-secrets-tag.png) - -### Comments - -To provide more context about a given secret, especially for your team, hover over it and press the comment button. - -![project comment secret](../../images/platform/project/project-secrets-comment.png) - -### Personal overrides - -Infisical employs the concept of **shared** and **personal** secrets to address the need -for common and custom secret values, or branching, amongst members of a team during software development. -To provide a helpful analogy: A shared value is to a `main` branch as a personal value is to a custom branch. - -Consider: - -- A team with users A, B, user C. -- A project with an environment containing a shared secret called D with the value E. - -Suppose user A overrides the value of secret D with the value F. - -Then: - -- If user A fetches the secret D back, they get the value F. -- If users B and C fetch the secret D back, they both get the value E. - - - Please keep in mind that secret reminders won't work with personal overrides. - - -![project override secret](../../images/platform/project/project-secrets-override.png) - -### Drawer - -To view the full details of each secret, you can hover over it and press on the ellipses button. - -![project secrets ellipses](../../images/platform/project/project-secrets-ellipses.png) - -This opens up a side-drawer: - -![project secrets drawer](../../images/platform/project/project-secrets-drawer.png) +Project settings are fully independent and reflect the capabilities of the associated product. diff --git a/docs/documentation/platform/secret-rotation/azure-client-secret.mdx b/docs/documentation/platform/secret-rotation/azure-client-secret.mdx index 046b772f6..c2da0a6a1 100644 --- a/docs/documentation/platform/secret-rotation/azure-client-secret.mdx +++ b/docs/documentation/platform/secret-rotation/azure-client-secret.mdx @@ -6,6 +6,7 @@ description: "Learn how to automatically rotate Azure Client Secrets." ## Prerequisites - Create an [Azure Client Secret Connection](/integrations/app-connections/azure-client-secrets). +- Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create an Azure Client Secret Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/ldap-password.mdx b/docs/documentation/platform/secret-rotation/ldap-password.mdx index feb3a664d..ba75e0af7 100644 --- a/docs/documentation/platform/secret-rotation/ldap-password.mdx +++ b/docs/documentation/platform/secret-rotation/ldap-password.mdx @@ -14,6 +14,7 @@ description: "Learn how to automatically rotate LDAP passwords." ## Prerequisites - Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements +- Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create an LDAP Password Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx index c20622f26..f609d11c5 100644 --- a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx @@ -30,6 +30,7 @@ An example creation statement might look like: To learn more about Microsoft SQL Server's permission system, please visit their [documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver16). +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create a Microsoft SQL Server Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/mysql-credentials.mdx b/docs/documentation/platform/secret-rotation/mysql-credentials.mdx index d0088a29e..349b5b893 100644 --- a/docs/documentation/platform/secret-rotation/mysql-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/mysql-credentials.mdx @@ -25,7 +25,7 @@ description: "Learn how to automatically rotate MySQL credentials." To learn more about the MySQL permission system, please visit their [documentation](https://dev.mysql.com/doc/refman/8.4/en/grant.html). - +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create a MySQL Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx b/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx index fb0887d33..0f72d62fa 100644 --- a/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx @@ -31,6 +31,7 @@ description: "Learn how to automatically rotate Oracle Database credentials." To learn more about the Oracle Database permission system, please visit their [documentation](https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-privilege-and-role-authorization.html). +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create an Oracle Database Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx index 55175d967..e8cddb0f4 100644 --- a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx @@ -27,6 +27,7 @@ description: "Learn how to automatically rotate PostgreSQL credentials." To learn more about PostgreSQL's permission system, please visit their [documentation](https://www.postgresql.org/docs/current/sql-grant.html). +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create a PostgreSQL Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secrets-mgmt/overview.mdx b/docs/documentation/platform/secrets-mgmt/overview.mdx new file mode 100644 index 000000000..e00eba358 --- /dev/null +++ b/docs/documentation/platform/secrets-mgmt/overview.mdx @@ -0,0 +1,17 @@ +--- +title: "Secrets Management" +sidebarTitle: "Overview" +description: "Learn how to securely store, access, and manage sensitive application secrets." +--- + +Infisical provides a flexible platform for managing application secrets — such as API keys, database credentials, application configuration, and more — across every stage of the development lifecycle from local development to production. + +It helps teams eliminate hardcoded secrets, enforce access controls, and adopt secure workflows like secret rotation, dynamic secrets, and secrets syncs to external platforms. + +Core capabilities include: + +- Secret Stores: Secure, versioned storage scoped by [project](/documentation/platform/secrets-mgmt/project), [environment](/documentation/platform/secrets-mgmt/project#project-environments), and [path](/documentation/platform/folder). +- [Access Control](/documentation/platform/access-controls/overview): Fine-grained, identity-aware permissions for users and machines +- Secret Delivery: Access secrets via [CLI](/cli/overview), [SDKs](/sdks/overview) (Go, Node.js, Python, etc.), [HTTP API](/api-reference/overview/introduction), [agents](/integrations/platforms/infisical-agent), [Kubernetes Operator](/integrations/platforms/kubernetes/overview), [External Secrets Operator (ESO)](https://external-secrets.io/latest/provider/infisical), and more. +- Lifecycle Automation: Automate [secret rotation](/documentation/platform/secret-rotation/overview), generate [dynamic secrets](/documentation/platform/dynamic-secrets/overview), and enforce [approval-based workflows](/documentation/platform/pr-workflows). +- [Secrets Syncs](/integrations/secret-syncs/overview): Push secrets to external services like [GitHub](/integrations/secret-syncs/github), [GitLab](/integrations/secret-syncs/gitlab), [AWS Secrets Manager](/integrations/secret-syncs/aws-secrets-manager), [Vercel](/integrations/secret-syncs/vercel), and more. diff --git a/docs/documentation/platform/secrets-mgmt/project.mdx b/docs/documentation/platform/secrets-mgmt/project.mdx new file mode 100644 index 000000000..3e7c7cbc7 --- /dev/null +++ b/docs/documentation/platform/secrets-mgmt/project.mdx @@ -0,0 +1,115 @@ +--- +title: "Projects" +description: "Learn more and understand the concept of Infisical projects." +--- + +A secrets management project in Infisical is a dedicated workspace for managing application secrets such as API keys, database credentials, configuration, etc. used by your applications. + +Secrets are organized into a clear hierarchy of environments, folders, and individual secrets, making it easy to manage values across different stages of your development lifecycle (e.g., development, staging, production). + +## Project environments + +For both visual and organizational structure, Infisical allows splitting up secrets into environments (e.g., development, staging, production). In project settings, such environments can be +customized depending on the intended use case. + +![project secrets overview](/images/platform/project/project-environments.png) + +## Secrets Overview + +The **Secrets Overview** page captures a birds-eye-view of secrets and [folders](./folder) across environments. +This is useful for comparing secrets, identifying if anything is missing, and making quick changes. + +![project secrets overview](/images/platform/project/project-secrets-overview-open.png) + +## Secrets Dashboard + +The **Secrets Dashboard** page appears when you press to manage the secrets of a specific environment. + +![project dashboard](/images/dashboard.png) + +### Secrets + +To add a secret, press **Add Secret** button at the top of the dashboard. + +![project add secret](/images/platform/project/project-secrets-add.png) + +For a new project, it can be convenient to populate the dashboard by dropping a `.env` file into the provided pane as shown below: + +![project drop env file](/images/platform/project/project-secrets-drop-env.png) + +To delete a secret, hover over it and press the **X** button that appears on the right side. + +![project delete secret](/images/platform/project/project-secrets-delete.png) + +To delete multiple secrets at once, hover over and select the secrets you'd like to delete +and press the **Delete** button that appears at the top. + +![project delete secret batch](/images/platform/project/project-secrets-delete-batch.png) + +### Search + +To search for specific secrets by their key name, you can use the search bar. + +![project search](/images/platform/project/project-secrets-search.png) + +To assist you with finding secrets, you can also group them by similar prefixes and filter them by tags (if applicable). + +![project filter](/images/platform/project/project-secrets-filter.png) + +### Hide/Un-hide + +To view/hide all secrets at once, toggle the hide or un-hide button. + +![project filter](/images/platform/project/project-secrets-unhide.png) + +### Download as .env + +To download/export secrets back into a `.env` file, press the download button. + +![project download back env](/images/platform/project/project-secrets-download-env.png) + +### Tags + +To better organize similar secrets, hover over them and label them with a tag. + +![project tag secret](/images/platform/project/project-secrets-tag.png) + +### Comments + +To provide more context about a given secret, especially for your team, hover over it and press the comment button. + +![project comment secret](/images/platform/project/project-secrets-comment.png) + +### Personal overrides + +Infisical employs the concept of **shared** and **personal** secrets to address the need +for common and custom secret values, or branching, amongst members of a team during software development. +To provide a helpful analogy: A shared value is to a `main` branch as a personal value is to a custom branch. + +Consider: + +- A team with users A, B, user C. +- A project with an environment containing a shared secret called D with the value E. + +Suppose user A overrides the value of secret D with the value F. + +Then: + +- If user A fetches the secret D back, they get the value F. +- If users B and C fetch the secret D back, they both get the value E. + + + Please keep in mind that secret reminders won't work with personal overrides. + + +![project override secret](/images/platform/project/project-secrets-override.png) + +### Drawer + +To view the full details of each secret, you can hover over it and press on the ellipses button. + +![project secrets ellipses](/images/platform/project/project-secrets-ellipses.png) + +This opens up a side-drawer: + +![project secrets drawer](/images/platform/project/project-secrets-drawer.png) diff --git a/docs/documentation/platform/ssh/host-groups.mdx b/docs/documentation/platform/ssh/host-groups.mdx index 877291183..373141e05 100644 --- a/docs/documentation/platform/ssh/host-groups.mdx +++ b/docs/documentation/platform/ssh/host-groups.mdx @@ -1,5 +1,5 @@ --- -title: "Infisical SSH" +title: "Host Groups" sidebarTitle: "Host Groups" description: "Learn how to organize SSH hosts into groups and manage access policies at scale." --- diff --git a/docs/documentation/platform/ssh/overview.mdx b/docs/documentation/platform/ssh/overview.mdx index a252e1f71..d9606214d 100644 --- a/docs/documentation/platform/ssh/overview.mdx +++ b/docs/documentation/platform/ssh/overview.mdx @@ -1,5 +1,5 @@ --- -title: "Infisical SSH" +title: "Overview" sidebarTitle: "Overview" description: "Learn how to securely provision user SSH access to your infrastructure using SSH certificates." --- diff --git a/docs/images/platform/events/access-dev.png b/docs/images/platform/events/access-dev.png new file mode 100644 index 000000000..3f44fc51b Binary files /dev/null and b/docs/images/platform/events/access-dev.png differ diff --git a/docs/images/platform/events/access-full.png b/docs/images/platform/events/access-full.png new file mode 100644 index 000000000..d41260c17 Binary files /dev/null and b/docs/images/platform/events/access-full.png differ diff --git a/docs/images/platform/events/access-path.png b/docs/images/platform/events/access-path.png new file mode 100644 index 000000000..3bfa9b28b Binary files /dev/null and b/docs/images/platform/events/access-path.png differ diff --git a/docs/images/platform/events/add-auth-method-form.png b/docs/images/platform/events/add-auth-method-form.png new file mode 100644 index 000000000..ba181ff48 Binary files /dev/null and b/docs/images/platform/events/add-auth-method-form.png differ diff --git a/docs/images/platform/events/add-auth-method.png b/docs/images/platform/events/add-auth-method.png new file mode 100644 index 000000000..583f13d09 Binary files /dev/null and b/docs/images/platform/events/add-auth-method.png differ diff --git a/docs/images/platform/events/add-conditions.png b/docs/images/platform/events/add-conditions.png new file mode 100644 index 000000000..bf0580b22 Binary files /dev/null and b/docs/images/platform/events/add-conditions.png differ diff --git a/docs/images/platform/events/add-policy.png b/docs/images/platform/events/add-policy.png new file mode 100644 index 000000000..00e6b76f0 Binary files /dev/null and b/docs/images/platform/events/add-policy.png differ diff --git a/docs/images/platform/events/copy-generated-token.png b/docs/images/platform/events/copy-generated-token.png new file mode 100644 index 000000000..1fb061017 Binary files /dev/null and b/docs/images/platform/events/copy-generated-token.png differ diff --git a/docs/images/platform/events/generate-auth-token-add.png b/docs/images/platform/events/generate-auth-token-add.png new file mode 100644 index 000000000..2057e2509 Binary files /dev/null and b/docs/images/platform/events/generate-auth-token-add.png differ diff --git a/docs/images/platform/events/generate-auth-token-create.png b/docs/images/platform/events/generate-auth-token-create.png new file mode 100644 index 000000000..9bccbb16f Binary files /dev/null and b/docs/images/platform/events/generate-auth-token-create.png differ diff --git a/docs/images/platform/events/generate-auth-token.png b/docs/images/platform/events/generate-auth-token.png new file mode 100644 index 000000000..40d6336a6 Binary files /dev/null and b/docs/images/platform/events/generate-auth-token.png differ diff --git a/docs/images/platform/events/identity-add-project.png b/docs/images/platform/events/identity-add-project.png new file mode 100644 index 000000000..5fdaa488e Binary files /dev/null and b/docs/images/platform/events/identity-add-project.png differ diff --git a/docs/images/platform/events/identity-add-role.png b/docs/images/platform/events/identity-add-role.png new file mode 100644 index 000000000..c20296cf8 Binary files /dev/null and b/docs/images/platform/events/identity-add-role.png differ diff --git a/docs/images/platform/events/identity-selector.png b/docs/images/platform/events/identity-selector.png new file mode 100644 index 000000000..2aaf7e56b Binary files /dev/null and b/docs/images/platform/events/identity-selector.png differ diff --git a/docs/images/platform/events/org-access-control.png b/docs/images/platform/events/org-access-control.png new file mode 100644 index 000000000..334d6dbc3 Binary files /dev/null and b/docs/images/platform/events/org-access-control.png differ diff --git a/docs/images/platform/events/policy-setting.png b/docs/images/platform/events/policy-setting.png new file mode 100644 index 000000000..cadab9b82 Binary files /dev/null and b/docs/images/platform/events/policy-setting.png differ diff --git a/docs/images/platform/events/postman-sse-response.png b/docs/images/platform/events/postman-sse-response.png new file mode 100644 index 000000000..fea1054b4 Binary files /dev/null and b/docs/images/platform/events/postman-sse-response.png differ diff --git a/docs/images/platform/events/postman-subscribe.png b/docs/images/platform/events/postman-subscribe.png new file mode 100644 index 000000000..308d257c0 Binary files /dev/null and b/docs/images/platform/events/postman-subscribe.png differ diff --git a/docs/images/platform/events/project-access.png b/docs/images/platform/events/project-access.png new file mode 100644 index 000000000..61593befd Binary files /dev/null and b/docs/images/platform/events/project-access.png differ diff --git a/docs/images/platform/events/project-detail.png b/docs/images/platform/events/project-detail.png new file mode 100644 index 000000000..65ff15642 Binary files /dev/null and b/docs/images/platform/events/project-detail.png differ diff --git a/docs/images/platform/events/project-role.png b/docs/images/platform/events/project-role.png new file mode 100644 index 000000000..b6178939f Binary files /dev/null and b/docs/images/platform/events/project-role.png differ diff --git a/docs/images/platform/events/role-detail.png b/docs/images/platform/events/role-detail.png new file mode 100644 index 000000000..7b4d6cea6 Binary files /dev/null and b/docs/images/platform/events/role-detail.png differ diff --git a/docs/images/platform/events/select-project.png b/docs/images/platform/events/select-project.png new file mode 100644 index 000000000..f964bf443 Binary files /dev/null and b/docs/images/platform/events/select-project.png differ diff --git a/docs/images/platform/identities/auth-templates/attach-template-form.png b/docs/images/platform/identities/auth-templates/attach-template-form.png new file mode 100644 index 000000000..7ac01d797 Binary files /dev/null and b/docs/images/platform/identities/auth-templates/attach-template-form.png differ diff --git a/docs/images/platform/identities/auth-templates/create-template-button.png b/docs/images/platform/identities/auth-templates/create-template-button.png new file mode 100644 index 000000000..859031ea3 Binary files /dev/null and b/docs/images/platform/identities/auth-templates/create-template-button.png differ diff --git a/docs/images/platform/identities/auth-templates/ldap-template.png b/docs/images/platform/identities/auth-templates/ldap-template.png new file mode 100644 index 000000000..04e9c5a1a Binary files /dev/null and b/docs/images/platform/identities/auth-templates/ldap-template.png differ diff --git a/docs/images/platform/identities/auth-templates/machine-identity-page.png b/docs/images/platform/identities/auth-templates/machine-identity-page.png new file mode 100644 index 000000000..4574afa3f Binary files /dev/null and b/docs/images/platform/identities/auth-templates/machine-identity-page.png differ diff --git a/docs/images/platform/identities/auth-templates/template-usages-modal.png b/docs/images/platform/identities/auth-templates/template-usages-modal.png new file mode 100644 index 000000000..7f53c7a45 Binary files /dev/null and b/docs/images/platform/identities/auth-templates/template-usages-modal.png differ diff --git a/docs/images/platform/identities/auth-templates/template-usages.png b/docs/images/platform/identities/auth-templates/template-usages.png new file mode 100644 index 000000000..67e71b53a Binary files /dev/null and b/docs/images/platform/identities/auth-templates/template-usages.png differ diff --git a/docs/images/platform/identities/auth-templates/templates-section.png b/docs/images/platform/identities/auth-templates/templates-section.png new file mode 100644 index 000000000..a26ebd820 Binary files /dev/null and b/docs/images/platform/identities/auth-templates/templates-section.png differ diff --git a/docs/images/platform/organization/organization-app-connections.png b/docs/images/platform/organization/organization-app-connections.png new file mode 100644 index 000000000..7da9221fd Binary files /dev/null and b/docs/images/platform/organization/organization-app-connections.png differ diff --git a/docs/images/platform/organization/organization-billing.png b/docs/images/platform/organization/organization-billing.png new file mode 100644 index 000000000..498812938 Binary files /dev/null and b/docs/images/platform/organization/organization-billing.png differ diff --git a/docs/images/platform/organization/organization-machine-identities.png b/docs/images/platform/organization/organization-machine-identities.png index 4400b9f62..85657e137 100644 Binary files a/docs/images/platform/organization/organization-machine-identities.png and b/docs/images/platform/organization/organization-machine-identities.png differ diff --git a/docs/images/platform/organization/organization-members-roles-add-perm.png b/docs/images/platform/organization/organization-members-roles-add-perm.png deleted file mode 100644 index 0e87e3e5f..000000000 Binary files a/docs/images/platform/organization/organization-members-roles-add-perm.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-members-roles.png b/docs/images/platform/organization/organization-members-roles.png deleted file mode 100644 index 08c2d1e90..000000000 Binary files a/docs/images/platform/organization/organization-members-roles.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-members.png b/docs/images/platform/organization/organization-members.png deleted file mode 100644 index a79d3bbe0..000000000 Binary files a/docs/images/platform/organization/organization-members.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-projects.png b/docs/images/platform/organization/organization-projects.png index 82c0aee57..b8a294632 100644 Binary files a/docs/images/platform/organization/organization-projects.png and b/docs/images/platform/organization/organization-projects.png differ diff --git a/docs/images/platform/organization/organization-roles.png b/docs/images/platform/organization/organization-roles.png new file mode 100644 index 000000000..04fd0043d Binary files /dev/null and b/docs/images/platform/organization/organization-roles.png differ diff --git a/docs/images/platform/organization/organization-settings-auth.png b/docs/images/platform/organization/organization-settings-auth.png deleted file mode 100644 index fd7a946e0..000000000 Binary files a/docs/images/platform/organization/organization-settings-auth.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-settings-general.png b/docs/images/platform/organization/organization-settings-general.png deleted file mode 100644 index 9467b6005..000000000 Binary files a/docs/images/platform/organization/organization-settings-general.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-settings.png b/docs/images/platform/organization/organization-settings.png new file mode 100644 index 000000000..873852b44 Binary files /dev/null and b/docs/images/platform/organization/organization-settings.png differ diff --git a/docs/images/platform/organization/organization-usage-billing.png b/docs/images/platform/organization/organization-usage-billing.png deleted file mode 100644 index 345a437f7..000000000 Binary files a/docs/images/platform/organization/organization-usage-billing.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-users.png b/docs/images/platform/organization/organization-users.png new file mode 100644 index 000000000..489fd2346 Binary files /dev/null and b/docs/images/platform/organization/organization-users.png differ diff --git a/docs/images/platform/organization/organization.png b/docs/images/platform/organization/organization.png new file mode 100644 index 000000000..f72c53286 Binary files /dev/null and b/docs/images/platform/organization/organization.png differ diff --git a/docs/images/platform/project/project-roles.png b/docs/images/platform/project/project-roles.png new file mode 100644 index 000000000..c7ae3f7f3 Binary files /dev/null and b/docs/images/platform/project/project-roles.png differ diff --git a/docs/images/platform/project/project-types.png b/docs/images/platform/project/project-types.png new file mode 100644 index 000000000..8156094f9 Binary files /dev/null and b/docs/images/platform/project/project-types.png differ diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 32fd0c12f..0a2d52ce1 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx index 91c606b0a..241c0350a 100644 --- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx index f4aaa7edd..34a8eb9a8 100644 --- a/docs/integrations/secret-syncs/azure-app-configuration.mdx +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal diff --git a/docs/integrations/secret-syncs/azure-devops.mdx b/docs/integrations/secret-syncs/azure-devops.mdx index 2f99fe128..b8c0331f8 100644 --- a/docs/integrations/secret-syncs/azure-devops.mdx +++ b/docs/integrations/secret-syncs/azure-devops.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a Azure DevOps Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [Azure DevOps Connection](/integrations/app-connections/azure-devops) +- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx index d19a0162e..e4cb82a04 100644 --- a/docs/integrations/secret-syncs/azure-key-vault.mdx +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx index afce51a0c..1ef31c997 100644 --- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -11,6 +11,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical." ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png) ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png) ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-service-usage-api.png) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index 14b2d9a7f..ed76c5edb 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create a [GitHub Connection](/integrations/app-connections/github) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/gitlab.mdx b/docs/integrations/secret-syncs/gitlab.mdx index 5e6cacffa..3f468ac0e 100644 --- a/docs/integrations/secret-syncs/gitlab.mdx +++ b/docs/integrations/secret-syncs/gitlab.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a GitLab Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create a [GitLab Connection](/integrations/app-connections/gitlab) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx index 396b4d13f..105d42395 100644 --- a/docs/integrations/secret-syncs/oci-vault.mdx +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -14,6 +14,7 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f - Create an [OCI Connection](/integrations/app-connections/oci) with the required **Secret Sync** permissions - [Create](https://docs.oracle.com/en-us/iaas/Content/Identity/compartments/To_create_a_compartment.htm) or use an existing OCI Compartment (which the OCI Connection is authorized to access) - [Create](https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/managingvaults_topic-To_create_a_new_vault.htm#createnewvault) or use an existing OCI Vault +- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/internals/permissions/organization-permissions.mdx b/docs/internals/permissions/organization-permissions.mdx index 80c843851..5f5fc962f 100644 --- a/docs/internals/permissions/organization-permissions.mdx +++ b/docs/internals/permissions/organization-permissions.mdx @@ -217,3 +217,14 @@ Supports conditions and permission inversion | `edit-gateways` | Modify existing gateway settings | | `delete-gateways` | Remove gateways from organization | | `attach-gateways` | Attach gateways to resources | + +#### Subject: `machine-identity-auth-template` + +| Action | Description | +| ------------------ | ---------------------------------------------- | +| `list-templates` | View identity auth templates | +| `create-templates` | Create new identity auth templates | +| `edit-templates` | Modify existing identity auth templates | +| `delete-templates` | Remove identity auth templates | +| `unlink-templates` | Unlink identity auth templates from identities | +| `attach-templates` | Attach identity auth templates to identities | diff --git a/frontend/src/components/auth/UserInfoStep.tsx b/frontend/src/components/auth/UserInfoStep.tsx index 2a1f8dca1..2eb6c65f6 100644 --- a/frontend/src/components/auth/UserInfoStep.tsx +++ b/frontend/src/components/auth/UserInfoStep.tsx @@ -1,12 +1,8 @@ -import crypto from "crypto"; - import { useState } from "react"; import { useTranslation } from "react-i18next"; import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import jsrp from "jsrp"; -import { useServerConfig } from "@app/context"; import { initProjectHelper } from "@app/helpers/project"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; @@ -14,15 +10,9 @@ import { onRequestError } from "@app/hooks/api/reactQuery"; import InputField from "../basic/InputField"; import checkPassword from "../utilities/checks/password/checkPassword"; -import Aes256Gcm from "../utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey, generateKeyPair } from "../utilities/cryptography/crypto"; -import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage"; import SecurityClient from "../utilities/SecurityClient"; import { Button, Input } from "../v2"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - interface UserInfoStepProps { incrementStep: () => void; email: string; @@ -76,7 +66,6 @@ export default function UserInfoStep({ }: UserInfoStepProps): JSX.Element { const [nameError, setNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false); - const { config } = useServerConfig(); const [errors, setErrors] = useState({}); @@ -108,109 +97,41 @@ export default function UserInfoStep({ }); if (!errorCheck) { - // Generate a random pair of a public and a private key - const pair = await generateKeyPair(config.fipsEnabled); + try { + const response = await completeAccountSignup({ + email, + password, + firstName: name.split(" ")[0], + lastName: name.split(" ").slice(1).join(" "), + providerAuthToken, + organizationName, + attributionSource + }); - localStorage.setItem("PRIVATE_KEY", pair.privateKey); + // unset signup JWT token and set JWT token + SecurityClient.setSignupToken(""); + SecurityClient.setToken(response.token); + SecurityClient.setProviderAuthToken(""); - client.init( - { - username: email, - password - }, - async () => { - client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { - try { - // TODO: moduralize into KeyService - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); - - if (!derivedKey) throw new Error("Failed to derive key from password"); - - const key = crypto.randomBytes(32); - - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: pair.privateKey, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - const response = await completeAccountSignup({ - email, - password, - firstName: name.split(" ")[0], - lastName: name.split(" ").slice(1).join(" "), - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey: pair.publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - providerAuthToken, - salt: result.salt, - verifier: result.verifier, - organizationName, - attributionSource - }); - - // unset signup JWT token and set JWT token - SecurityClient.setSignupToken(""); - SecurityClient.setToken(response.token); - SecurityClient.setProviderAuthToken(""); - - if (response.organizationId) { - await selectOrganization({ organizationId: response.organizationId }); - } - - saveTokenToLocalStorage({ - publicKey: pair.publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey: pair.privateKey - }); - - const userOrgs = await fetchOrganizations(); - - const orgId = userOrgs[0]?.id; - await initProjectHelper({ - projectName: "Example Project" - }); - - localStorage.setItem("orgData.id", orgId); - - incrementStep(); - } catch (error) { - onRequestError(error); - setIsLoading(false); - console.error(error); - } - }); + if (response.organizationId) { + await selectOrganization({ organizationId: response.organizationId }); } - ); + + const userOrgs = await fetchOrganizations(); + + const orgId = userOrgs[0]?.id; + await initProjectHelper({ + projectName: "Example Project" + }); + + localStorage.setItem("orgData.id", orgId); + + incrementStep(); + } catch (error) { + onRequestError(error); + setIsLoading(false); + console.error(error); + } } else { setIsLoading(false); } diff --git a/frontend/src/components/utilities/attemptChangePassword.ts b/frontend/src/components/utilities/attemptChangePassword.ts deleted file mode 100644 index e51168525..000000000 --- a/frontend/src/components/utilities/attemptChangePassword.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* eslint-disable new-cap */ -import crypto from "crypto"; - -import jsrp from "jsrp"; - -import { changePassword, srp1 } from "@app/hooks/api/auth/queries"; - -import Aes256Gcm from "./cryptography/aes-256-gcm"; -import { deriveArgonKey } from "./cryptography/crypto"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; - -const clientOldPassword = new jsrp.client(); -const clientNewPassword = new jsrp.client(); - -type Params = { - email: string; - currentPassword: string; - newPassword: string; -}; - -const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise => { - return new Promise((resolve, reject) => { - clientOldPassword.init({ username: email, password: currentPassword }, async () => { - let serverPublicKey; - let salt; - - try { - const clientPublicKey = clientOldPassword.getPublicKey(); - - const res = await srp1({ clientPublicKey }); - - serverPublicKey = res.serverPublicKey; - salt = res.salt; - - clientOldPassword.setSalt(salt); - clientOldPassword.setServerPublicKey(serverPublicKey); - - const clientProof = clientOldPassword.getProof(); - - clientNewPassword.init({ username: email, password: newPassword }, async () => { - clientNewPassword.createVerifier(async (_err, result) => { - try { - const derivedKey = await deriveArgonKey({ - password: newPassword, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); - - if (!derivedKey) throw new Error("Failed to derive key from password"); - - const key = crypto.randomBytes(32); - - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: localStorage.getItem("PRIVATE_KEY") as string, - secret: key - }); - - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - await changePassword({ - password: newPassword, - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt: result.salt, - verifier: result.verifier - }); - - saveTokenToLocalStorage({ - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - }); - - resolve(); - } catch (err2) { - console.error(err2); - reject(err2); - } - }); - }); - } catch (err) { - console.error(err); - reject(err); - } - }); - }); -}; - -export default attemptChangePassword; diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index d897ebeaa..f8339aecf 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -1,18 +1,18 @@ /* eslint-disable prefer-destructuring */ +import axios from "axios"; import jsrp from "jsrp"; -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, login2 } from "@app/hooks/api/auth/queries"; +import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries"; +import { createNotification } from "../notifications"; import Telemetry from "./telemetry/Telemetry"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; +import { LoginMode } from "./attemptLogin"; import SecurityClient from "./SecurityClient"; // eslint-disable-next-line new-cap const client = new jsrp.client(); export interface IsCliLoginSuccessful { - mfaEnabled: boolean; loginResponse?: { email: string; privateKey: string; @@ -31,14 +31,62 @@ const attemptLogin = async ({ email, password, providerAuthToken, - captchaToken + captchaToken, + loginMode = LoginMode.ServerSide }: { email: string; password: string; providerAuthToken?: string; captchaToken?: string; + loginMode?: LoginMode; }): Promise => { const telemetry = new Telemetry().getInstance(); + + if (loginMode === LoginMode.ServerSide) { + console.log("Attempting login with server side..."); + const data = await loginV3({ + email, + password, + providerAuthToken, + captchaToken + }).catch((err) => { + if (axios.isAxiosError(err) && err.response?.status === 400) { + if (err.response?.data?.error === "LegacyEncryptionScheme") { + createNotification({ + text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.", + type: "info" + }); + + return null; + } + } + + throw err; + }); + + if (data === null) { + return attemptLogin({ + email, + password, + providerAuthToken, + captchaToken, + loginMode: LoginMode.LegacySrp + }); + } + + SecurityClient.setProviderAuthToken(""); + SecurityClient.setToken(data.accessToken); + + return { + success: true, + loginResponse: { + email, + privateKey: "", + JTWToken: data.accessToken + } + }; + } + return new Promise((resolve, reject) => { client.init( { @@ -58,79 +106,26 @@ const attemptLogin = async ({ client.setServerPublicKey(serverPublicKey); const clientProof = client.getProof(); // called M1 - const { - mfaEnabled, - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await login2({ + const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({ email, password, clientProof, providerAuthToken, captchaToken }); - if (mfaEnabled) { - // case: MFA is enabled - - // set temporary (MFA) JWT token - SecurityClient.setMfaToken(token); - - resolve({ - mfaEnabled, - success: true - }); - } else if ( - !mfaEnabled && - encryptionVersion && - encryptedPrivateKey && - iv && - tag && - token - ) { - // case: MFA is not enabled - - // unset provider auth token in case it was used + if (encryptionVersion && encryptedPrivateKey && iv && tag && token) { SecurityClient.setProviderAuthToken(""); - // set JWT token SecurityClient.setToken(token); - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - if (email) { telemetry.identify(email, email); telemetry.capture("User Logged In"); } resolve({ - mfaEnabled: false, loginResponse: { email, - privateKey, + privateKey: "", JTWToken: token }, success: true diff --git a/frontend/src/components/utilities/attemptCliLoginMfa.ts b/frontend/src/components/utilities/attemptCliLoginMfa.ts deleted file mode 100644 index 14a61d817..000000000 --- a/frontend/src/components/utilities/attemptCliLoginMfa.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-disable prefer-destructuring */ -import jsrp from "jsrp"; - -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries"; - -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; -import SecurityClient from "./SecurityClient"; - -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - -interface IsMfaLoginSuccessful { - success: boolean; - loginResponse: { - privateKey: string; - JTWToken: string; - }; -} - -/** - * Return whether or not MFA-login is successful for user with email [email] - * and MFA token [mfaToken] - * @param {Object} obj - * @param {String} obj.email - email of user - * @param {String} obj.mfaToken - MFA code/token - */ -const attemptLoginMfa = async ({ - email, - password, - providerAuthToken, - mfaToken -}: { - email: string; - password: string; - providerAuthToken?: string; - mfaToken: string; -}): Promise => { - return new Promise((resolve, reject) => { - client.init( - { - username: email, - password - }, - async () => { - try { - const clientPublicKey = client.getPublicKey(); - const { salt } = await login1({ - email, - clientPublicKey, - providerAuthToken - }); - - const { - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await verifyMfaToken({ - email, - mfaCode: mfaToken - }); - - // unset temporary (MFA) JWT token and set JWT token - SecurityClient.setMfaToken(""); - SecurityClient.setToken(token); - SecurityClient.setProviderAuthToken(""); - - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - - resolve({ - success: true, - loginResponse: { - privateKey, - JTWToken: token - } - }); - } catch (err) { - reject(err); - } - } - ); - }); -}; - -export default attemptLoginMfa; diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index c371d9c91..bdaf79042 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -1,15 +1,19 @@ /* eslint-disable prefer-destructuring */ +import axios from "axios"; import jsrp from "jsrp"; -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, login2 } from "@app/hooks/api/auth/queries"; +import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries"; +import { createNotification } from "../notifications"; import Telemetry from "./telemetry/Telemetry"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; import SecurityClient from "./SecurityClient"; +export enum LoginMode { + LegacySrp = "legacy-srp", + ServerSide = "server-side" +} + interface IsLoginSuccessful { - mfaEnabled: boolean; success: boolean; } @@ -23,14 +27,62 @@ const attemptLogin = async ({ email, password, providerAuthToken, - captchaToken + captchaToken, + loginMode = LoginMode.ServerSide }: { email: string; password: string; providerAuthToken?: string; captchaToken?: string; + loginMode?: LoginMode; }): Promise => { const telemetry = new Telemetry().getInstance(); + + if (loginMode === LoginMode.ServerSide) { + console.log("Attempting login with server side..."); + const data = await loginV3({ + email, + password, + providerAuthToken, + captchaToken + }).catch((err) => { + if (axios.isAxiosError(err) && err.response?.status === 400) { + if (err.response?.data?.error === "LegacyEncryptionScheme") { + createNotification({ + text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.", + type: "info" + }); + + return null; + } + } + + throw err; + }); + + if (data === null) { + return attemptLogin({ + email, + password, + providerAuthToken, + captchaToken, + loginMode: LoginMode.LegacySrp + }); + } + + SecurityClient.setProviderAuthToken(""); + SecurityClient.setToken(data.accessToken); + + if (email) { + telemetry.identify(email, email); + telemetry.capture("User Logged In"); + } + + return { + success: true + }; + } + // eslint-disable-next-line new-cap const client = new jsrp.client(); await new Promise((resolve) => { @@ -48,18 +100,7 @@ const attemptLogin = async ({ client.setServerPublicKey(serverPublicKey); const clientProof = client.getProof(); // called M1 - const { - mfaEnabled, - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await login2({ + const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({ captchaToken, email, password, @@ -67,56 +108,22 @@ const attemptLogin = async ({ providerAuthToken }); - if (mfaEnabled) { - // case: MFA is enabled - - // set temporary (MFA) JWT token - SecurityClient.setMfaToken(token); - - return { - mfaEnabled, - success: true - }; - } - if (!mfaEnabled && encryptionVersion && encryptedPrivateKey && iv && tag && token) { - // case: MFA is not enabled - + if (encryptionVersion && encryptedPrivateKey && iv && tag && token) { // unset provider auth token in case it was used SecurityClient.setProviderAuthToken(""); // set JWT token SecurityClient.setToken(token); - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - if (email) { telemetry.identify(email, email); telemetry.capture("User Logged In"); } return { - mfaEnabled: false, success: true }; } - return { success: false, mfaEnabled: false }; + return { success: false }; }; export default attemptLogin; diff --git a/frontend/src/components/utilities/attemptLoginMfa.ts b/frontend/src/components/utilities/attemptLoginMfa.ts deleted file mode 100644 index 10f36ec39..000000000 --- a/frontend/src/components/utilities/attemptLoginMfa.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable prefer-destructuring */ -import jsrp from "jsrp"; - -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries"; - -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; -import SecurityClient from "./SecurityClient"; - -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - -/** - * Return whether or not MFA-login is successful for user with email [email] - * and MFA token [mfaToken] - * @param {Object} obj - * @param {String} obj.email - email of user - * @param {String} obj.mfaToken - MFA code/token - */ -const attemptLoginMfa = async ({ - email, - password, - providerAuthToken, - mfaToken -}: { - email: string; - password: string; - providerAuthToken?: string; - mfaToken: string; -}): Promise => { - return new Promise((resolve, reject) => { - client.init( - { - username: email, - password - }, - async () => { - try { - const clientPublicKey = client.getPublicKey(); - const { salt } = await login1({ - email, - clientPublicKey, - providerAuthToken - }); - - const { - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await verifyMfaToken({ - email, - mfaCode: mfaToken - }); - - // unset temporary (MFA) JWT token and set JWT token - SecurityClient.setMfaToken(""); - SecurityClient.setToken(token); - SecurityClient.setProviderAuthToken(""); - - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - - resolve(true); - } catch (err) { - reject(err); - } - } - ); - }); -}; - -export default attemptLoginMfa; diff --git a/frontend/src/components/utilities/cryptography/issueBackupKey.ts b/frontend/src/components/utilities/cryptography/issueBackupKey.ts deleted file mode 100644 index 52a502e4e..000000000 --- a/frontend/src/components/utilities/cryptography/issueBackupKey.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable new-cap */ -import crypto from "crypto"; - -import jsrp from "jsrp"; - -import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries"; - -import generateBackupPDF from "../generateBackupPDF"; -import Aes256Gcm from "./aes-256-gcm"; - -const clientPassword = new jsrp.client(); -const clientKey = new jsrp.client(); - -interface BackupKeyProps { - email: string; - password: string; - personalName: string; - setBackupKeyError: (value: boolean) => void; - setBackupKeyIssued: (value: boolean) => void; -} - -/** - * This function issue a backup key for a user - * @param {obkect} obj - * @param {string} obj.email - email of a user issuing a backup key - * @param {string} obj.password - password of a user issuing a backup key - * @param {string} obj.personalName - name of a user issuing a backup key - * @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key - * @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly - * @returns - */ -const issueBackupKey = async ({ - email, - password, - personalName, - setBackupKeyError, - setBackupKeyIssued -}: BackupKeyProps) => { - try { - setBackupKeyError(false); - setBackupKeyIssued(false); - clientPassword.init( - { - username: email, - password - }, - async () => { - const clientPublicKey = clientPassword.getPublicKey(); - - let serverPublicKey; - let salt; - try { - const res = await srp1({ - clientPublicKey - }); - serverPublicKey = res.serverPublicKey; - salt = res.salt; - } catch (err) { - setBackupKeyError(true); - console.log("Wrong current password", err, 1); - } - - clientPassword.setSalt(salt as string); - clientPassword.setServerPublicKey(serverPublicKey as string); - const clientProof = clientPassword.getProof(); // called M1 - - const generatedKey = crypto.randomBytes(16).toString("hex"); - - clientKey.init( - { - username: email, - password: generatedKey - }, - async () => { - clientKey.createVerifier( - async (_err: any, result: { salt: string; verifier: string }) => { - const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ - text: String(localStorage.getItem("PRIVATE_KEY")), - secret: generatedKey - }); - - try { - await issueBackupPrivateKey({ - encryptedPrivateKey: ciphertext, - iv, - tag, - salt: result.salt, - verifier: result.verifier, - clientProof - }); - - generateBackupPDF({ - personalName, - personalEmail: email, - generatedKey - }); - setBackupKeyIssued(true); - } catch { - setBackupKeyError(true); - } - } - ); - } - ); - } - ); - } catch { - setBackupKeyError(true); - console.log("Failed to issue a backup key"); - } - return true; -}; - -export default issueBackupKey; diff --git a/frontend/src/components/utilities/saveTokenToLocalStorage.ts b/frontend/src/components/utilities/saveTokenToLocalStorage.ts deleted file mode 100644 index 1539438f4..000000000 --- a/frontend/src/components/utilities/saveTokenToLocalStorage.ts +++ /dev/null @@ -1,67 +0,0 @@ -interface Props { - protectedKey?: string; - protectedKeyIV?: string; - protectedKeyTag?: string; - publicKey?: string; - encryptedPrivateKey?: string; - iv?: string; - tag?: string; - privateKey?: string; -} - -export const saveTokenToLocalStorage = ({ - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey -}: Props) => { - try { - if (protectedKey) { - localStorage.removeItem("protectedKey"); - localStorage.setItem("protectedKey", protectedKey); - } - - if (protectedKeyIV) { - localStorage.removeItem("protectedKeyIV"); - localStorage.setItem("protectedKeyIV", protectedKeyIV); - } - - if (protectedKeyTag) { - localStorage.removeItem("protectedKeyTag"); - localStorage.setItem("protectedKeyTag", protectedKeyTag); - } - - if (publicKey) { - localStorage.removeItem("publicKey"); - localStorage.setItem("publicKey", publicKey); - } - - if (encryptedPrivateKey) { - localStorage.removeItem("encryptedPrivateKey"); - localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey); - } - - if (iv) { - localStorage.removeItem("iv"); - localStorage.setItem("iv", iv); - } - - if (tag) { - localStorage.removeItem("tag"); - localStorage.setItem("tag", tag); - } - - if (privateKey) { - localStorage.removeItem("PRIVATE_KEY"); - localStorage.setItem("PRIVATE_KEY", privateKey); - } - } catch (err) { - if (err instanceof Error) { - throw new Error(`Unable to send the tokens in local storage:${err.message}`); - } - } -}; diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 59446eb07..50e147aa0 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -21,6 +21,15 @@ export enum OrgGatewayPermissionActions { AttachGateways = "attach-gateways" } +export enum OrgPermissionMachineIdentityAuthTemplateActions { + ListTemplates = "list-templates", + CreateTemplates = "create-templates", + EditTemplates = "edit-templates", + DeleteTemplates = "delete-templates", + UnlinkTemplates = "unlink-templates", + AttachTemplates = "attach-templates" +} + export enum OrgPermissionSubjects { Workspace = "workspace", Role = "role", @@ -42,7 +51,8 @@ export enum OrgPermissionSubjects { Kmip = "kmip", Gateway = "gateway", SecretShare = "secret-share", - GithubOrgSync = "github-org-sync" + GithubOrgSync = "github-org-sync", + MachineIdentityAuthTemplate = "machine-identity-auth-template" } export enum OrgPermissionAdminConsoleAction { @@ -113,6 +123,10 @@ export type OrgPermissionSet = | [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections] | [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity] | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip] + | [ + OrgPermissionMachineIdentityAuthTemplateActions, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ] | [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway] | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]; // TODO(scott): add back once org UI refactored diff --git a/frontend/src/hooks/api/auditLogs/queries.tsx b/frontend/src/hooks/api/auditLogs/queries.tsx index b254b46d3..8cb438c92 100644 --- a/frontend/src/hooks/api/auditLogs/queries.tsx +++ b/frontend/src/hooks/api/auditLogs/queries.tsx @@ -1,8 +1,7 @@ import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { AxiosError } from "axios"; -import { createNotification } from "@app/components/notifications"; import { apiRequest } from "@app/config/request"; +import { onRequestError } from "@app/hooks/api/reactQuery"; import { TReactQueryOptions } from "@app/types/reactQuery"; import { Actor, AuditLog, TGetAuditLogsFilter } from "./types"; @@ -46,12 +45,7 @@ export const useGetAuditLogs = ( ); return data.auditLogs; } catch (error) { - if (error instanceof AxiosError) { - createNotification({ - type: "error", - text: error.response?.data.message - }); - } + onRequestError(error); return []; } }, diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 796fd3152..cb7e3f64c 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -8,26 +8,24 @@ import { organizationKeys } from "../organization/queries"; import { setAuthToken } from "../reactQuery"; import { workspaceKeys } from "../workspace"; import { - ChangePasswordDTO, CompleteAccountDTO, CompleteAccountSignupDTO, GetAuthTokenAPI, GetBackupEncryptedPrivateKeyDTO, - IssueBackupPrivateKeyDTO, Login1DTO, Login1Res, Login2DTO, Login2Res, LoginLDAPDTO, LoginLDAPRes, + LoginV3DTO, + LoginV3Res, MfaMethod, ResetPasswordDTO, ResetPasswordV2DTO, ResetUserPasswordV2DTO, SendMfaTokenDTO, SetupPasswordDTO, - SRP1DTO, - SRPR1Res, TOauthTokenExchangeDTO, UserAgentType, UserEncryptionVersion, @@ -50,21 +48,14 @@ export const login2 = async (loginDetails: Login2DTO) => { return data; }; -export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => { - const { data } = await apiRequest.post("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token +export const loginV3 = async (loginDetails: LoginV3DTO) => { + const { data } = await apiRequest.post("/api/v3/auth/login", loginDetails); return data; }; -export const useLogin1 = () => { - return useMutation({ - mutationFn: async (details: { - email: string; - clientPublicKey: string; - providerAuthToken?: string; - }) => { - return login1(details); - } - }); +export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => { + const { data } = await apiRequest.post("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token + return data; }; export const selectOrganization = async (data: { @@ -143,11 +134,6 @@ export const useOauthTokenExchange = () => { }); }; -export const srp1 = async (details: SRP1DTO) => { - const { data } = await apiRequest.post("/api/v1/password/srp1", details); - return data; -}; - export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => { const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details); return data; @@ -158,14 +144,6 @@ export const completeAccountSignupInvite = async (details: CompleteAccountDTO) = return data; }; -export const useCompleteAccountSignup = () => { - return useMutation({ - mutationFn: async (details: CompleteAccountSignupDTO) => { - return completeAccountSignup(details); - } - }); -}; - export const useSendMfaToken = () => { return useMutation({ mutationFn: async ({ email }) => { @@ -263,11 +241,6 @@ export const useVerifyPasswordResetCode = () => { }); }; -export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => { - const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details); - return data; -}; - export const getBackupEncryptedPrivateKey = async ({ verificationToken }: GetBackupEncryptedPrivateKeyDTO) => { @@ -328,20 +301,6 @@ export const useResetUserPasswordV2 = () => { }); }; -export const changePassword = async (details: ChangePasswordDTO) => { - const { data } = await apiRequest.post("/api/v1/password/change-password", details); - return data; -}; - -export const useChangePassword = () => { - // note: use after srp1 - return useMutation({ - mutationFn: async (details: ChangePasswordDTO) => { - return changePassword(details); - } - }); -}; - // Refresh token is set as cookie when logged in // Using that we fetch the auth bearer token needed for auth calls export const fetchAuthToken = async () => { diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index cbd20b643..fd9d6fb5c 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -49,13 +49,19 @@ export type Login2DTO = { password: string; }; +export type LoginV3DTO = { + email: string; + password: string; + providerAuthToken?: string; + captchaToken?: string; +}; + export type Login1Res = { serverPublicKey: string; salt: string; }; export type Login2Res = { - mfaEnabled: boolean; token: string; encryptionVersion?: number; protectedKey?: string; @@ -67,6 +73,11 @@ export type Login2Res = { tag?: string; }; +export type LoginV3Res = { + accessToken: string; + mfaEnabled: boolean; +}; + export type LoginLDAPDTO = { organizationSlug: string; username: string; @@ -77,28 +88,10 @@ export type LoginLDAPRes = { nextUrl: string; }; -export type SRP1DTO = { - clientPublicKey: string; -}; - -export type SRPR1Res = { - serverPublicKey: string; - salt: string; -}; - export type CompleteAccountDTO = { email: string; firstName: string; lastName: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; password: string; tokenMetadata?: string; }; @@ -116,19 +109,6 @@ export type VerifySignupInviteDTO = { organizationId: string; }; -export type ChangePasswordDTO = { - password: string; - clientProof: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; -}; - export type ResetPasswordDTO = { protectedKey: string; protectedKeyIV: string; @@ -153,27 +133,11 @@ export type ResetUserPasswordV2DTO = { }; export type SetupPasswordDTO = { - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; + email: string; token: string; password: string; }; -export type IssueBackupPrivateKeyDTO = { - encryptedPrivateKey: string; - iv: string; - tag: string; - salt: string; - verifier: string; - clientProof: string; -}; - export type GetBackupEncryptedPrivateKeyDTO = { verificationToken: string; }; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 165c94395..c01f1aa85 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -1385,6 +1385,7 @@ export const useAddIdentityLdapAuth = () => { return useMutation({ mutationFn: async ({ identityId, + templateId, url, bindDN, bindPass, @@ -1400,6 +1401,7 @@ export const useAddIdentityLdapAuth = () => { const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>( `/api/v1/auth/ldap-auth/identities/${identityId}`, { + templateId, url, bindDN, bindPass, @@ -1432,6 +1434,7 @@ export const useUpdateIdentityLdapAuth = () => { return useMutation({ mutationFn: async ({ identityId, + templateId, url, bindDN, bindPass, @@ -1447,6 +1450,7 @@ export const useUpdateIdentityLdapAuth = () => { const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>( `/api/v1/auth/ldap-auth/identities/${identityId}`, { + templateId, url, bindDN, bindPass, diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 1af1cc24c..c0e49e987 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -567,10 +567,11 @@ export type IdentityTokenAuth = { export type AddIdentityLdapAuthDTO = { organizationId: string; identityId: string; - url: string; - bindDN: string; - bindPass: string; - searchBase: string; + templateId?: string; + url?: string; + bindDN?: string; + bindPass?: string; + searchBase?: string; searchFilter: string; ldapCaCertificate?: string; allowedFields?: { @@ -588,6 +589,7 @@ export type AddIdentityLdapAuthDTO = { export type UpdateIdentityLdapAuthDTO = { identityId: string; organizationId: string; + templateId?: string; url?: string; bindDN?: string; bindPass?: string; @@ -612,10 +614,11 @@ export type DeleteIdentityLdapAuthDTO = { }; export type IdentityLdapAuth = { - url: string; - bindDN: string; - bindPass: string; - searchBase: string; + url?: string; + bindDN?: string; + templateId?: string; + bindPass?: string; + searchBase?: string; searchFilter: string; ldapCaCertificate?: string; allowedFields?: { diff --git a/frontend/src/hooks/api/identityAuthTemplates/index.tsx b/frontend/src/hooks/api/identityAuthTemplates/index.tsx new file mode 100644 index 000000000..177955438 --- /dev/null +++ b/frontend/src/hooks/api/identityAuthTemplates/index.tsx @@ -0,0 +1,3 @@ +export * from "./mutations"; +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/identityAuthTemplates/mutations.tsx b/frontend/src/hooks/api/identityAuthTemplates/mutations.tsx new file mode 100644 index 000000000..8c05f1e46 --- /dev/null +++ b/frontend/src/hooks/api/identityAuthTemplates/mutations.tsx @@ -0,0 +1,97 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { identityAuthTemplatesKeys } from "./queries"; +import { + CreateIdentityAuthTemplateDTO, + DeleteIdentityAuthTemplateDTO, + IdentityAuthTemplate, + MachineAuthTemplateUsage, + UnlinkTemplateUsageDTO, + UpdateIdentityAuthTemplateDTO +} from "./types"; + +export const useCreateIdentityAuthTemplate = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => { + const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>( + "/api/v1/identity-templates", + dto + ); + return data.template; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId }) + }); + } + }); +}; + +export const useUpdateIdentityAuthTemplate = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => { + const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>( + `/api/v1/identity-templates/${dto.templateId}`, + dto + ); + return data.template; + }, + onSuccess: (_, { organizationId, templateId }) => { + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId }) + }); + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplate(templateId) + }); + } + }); +}; + +export const useDeleteIdentityAuthTemplate = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto: DeleteIdentityAuthTemplateDTO) => { + await apiRequest.delete(`/api/v1/identity-templates/${dto.templateId}`, { + params: { organizationId: dto.organizationId } + }); + }, + onSuccess: (_, { organizationId, templateId }) => { + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId }) + }); + queryClient.removeQueries({ + queryKey: identityAuthTemplatesKeys.getTemplate(templateId) + }); + } + }); +}; + +export const useUnlinkTemplateUsage = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto: UnlinkTemplateUsageDTO) => { + const { data } = await apiRequest.post( + `/api/v1/identity-templates/${dto.templateId}/delete-usage`, + { identityIds: dto.identityIds }, + { params: { organizationId: dto.organizationId } } + ); + return data; + }, + onSuccess: (_, { templateId, organizationId }) => { + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplateUsages(templateId) + }); + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId }) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/identityAuthTemplates/queries.tsx b/frontend/src/hooks/api/identityAuthTemplates/queries.tsx new file mode 100644 index 000000000..a9981c0c4 --- /dev/null +++ b/frontend/src/hooks/api/identityAuthTemplates/queries.tsx @@ -0,0 +1,89 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { + GetIdentityAuthTemplatesDTO, + GetTemplateUsagesDTO, + IdentityAuthTemplate, + MachineAuthTemplateUsage, + MachineIdentityAuthMethod +} from "./types"; + +export const identityAuthTemplatesKeys = { + all: ["identity-auth-templates"] as const, + getTemplates: (dto: GetIdentityAuthTemplatesDTO) => + [...identityAuthTemplatesKeys.all, "list", dto] as const, + getTemplate: (templateId: string) => + [...identityAuthTemplatesKeys.all, "single", templateId] as const, + getAvailableTemplates: (authMethod: MachineIdentityAuthMethod) => + [...identityAuthTemplatesKeys.all, "available", authMethod] as const, + getTemplateUsages: (templateId: string) => + [...identityAuthTemplatesKeys.all, "usages", templateId] as const +}; + +export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) => { + return useQuery({ + queryKey: identityAuthTemplatesKeys.getTemplates(dto), + queryFn: async () => { + const { data } = await apiRequest.get<{ + templates: IdentityAuthTemplate[]; + totalCount: number; + }>("/api/v1/identity-templates/search", { + params: { + organizationId: dto.organizationId, + limit: dto.limit || 50, + offset: dto.offset || 0, + ...(dto.search && { search: dto.search }) + } + }); + return data; + }, + enabled: Boolean(dto.organizationId) + }); +}; + +export const useGetIdentityAuthTemplate = (templateId: string, organizationId: string) => { + return useQuery({ + queryKey: identityAuthTemplatesKeys.getTemplate(templateId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/identity-templates/${templateId}`, + { + params: { organizationId } + } + ); + return data; + }, + enabled: Boolean(templateId) && Boolean(organizationId) + }); +}; + +export const useGetAvailableTemplates = (authMethod: MachineIdentityAuthMethod) => { + return useQuery({ + queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod), + queryFn: async () => { + const { data } = await apiRequest.get("/api/v1/identity-templates", { + params: { authMethod } + }); + return data; + }, + enabled: Boolean(authMethod) + }); +}; + +export const useGetTemplateUsages = (dto: GetTemplateUsagesDTO) => { + return useQuery({ + queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/identity-templates/${dto.templateId}/usage`, + { + params: { organizationId: dto.organizationId } + } + ); + return data; + }, + enabled: Boolean(dto.templateId) && Boolean(dto.organizationId) + }); +}; diff --git a/frontend/src/hooks/api/identityAuthTemplates/types.ts b/frontend/src/hooks/api/identityAuthTemplates/types.ts new file mode 100644 index 000000000..860f9a9fc --- /dev/null +++ b/frontend/src/hooks/api/identityAuthTemplates/types.ts @@ -0,0 +1,78 @@ +export enum MachineIdentityAuthMethod { + LDAP = "ldap" +} + +export interface LdapTemplateFields { + url: string; + bindDN: string; + bindPass: string; + searchBase: string; + ldapCaCertificate?: string; +} + +export interface IdentityAuthTemplate { + id: string; + name: string; + authMethod: MachineIdentityAuthMethod; + organizationId: string; + templateFields: LdapTemplateFields; + createdAt: string; + updatedAt: string; +} + +export interface CreateIdentityAuthTemplateDTO { + organizationId: string; + name: string; + authMethod: MachineIdentityAuthMethod; + templateFields: LdapTemplateFields; +} + +export interface UpdateIdentityAuthTemplateDTO { + templateId: string; + organizationId: string; + name?: string; + templateFields?: Partial; +} + +export interface DeleteIdentityAuthTemplateDTO { + templateId: string; + organizationId: string; +} + +export interface GetIdentityAuthTemplatesDTO { + organizationId: string; + limit?: number; + offset?: number; + search?: string; +} + +export interface MachineAuthTemplateUsage { + identityId: string; + identityName: string; +} + +export interface GetTemplateUsagesDTO { + templateId: string; + organizationId: string; +} + +export interface UnlinkTemplateUsageDTO { + templateId: string; + identityIds: string[]; + organizationId: string; +} + +export const TEMPLATE_ERROR_MESSAGES = { + UNLINK_SUCCESS: "Successfully unlinked template usages", + UNLINK_FAILED: "Failed to unlink template usages", + SINGLE_UNLINK_SUCCESS: "Successfully unlinked template usage", + SINGLE_UNLINK_FAILED: "Failed to unlink template usage" +} as const; + +export const TEMPLATE_UI_LABELS = { + VIEW_USAGES: "View Usages", + EDIT_TEMPLATE: "Edit Template", + DELETE_TEMPLATE: "Delete Template", + UNLINK: "Unlink", + UNSELECT_ALL: "Unselect All" +} as const; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 4b4967f16..78dcd3b79 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -15,6 +15,7 @@ export * from "./gateways"; export * from "./githubOrgSyncConfig"; export * from "./groups"; export * from "./identities"; +export * from "./identityAuthTemplates"; export * from "./identityProjectAdditionalPrivilege"; export * from "./incidentContacts"; export * from "./integrationAuth"; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 87a02231f..4ded71cfe 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -53,4 +53,5 @@ export type SubscriptionPlan = { secretScanning: boolean; enterpriseSecretSyncs: boolean; enterpriseAppConnections: boolean; + machineIdentityAuthTemplates: boolean; }; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 75edc0907..77289fee6 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -495,14 +495,6 @@ export const useGetMyOrganizationProjects = (orgId: string) => { }); }; -export const fetchMyPrivateKey = async () => { - const { - data: { privateKey } - } = await apiRequest.get<{ privateKey: string }>("/api/v1/user/private-key"); - - return privateKey; -}; - export const useListUserGroupMemberships = (username: string) => { return useQuery({ queryKey: userKeys.listUserGroupMemberships(username), diff --git a/frontend/src/lib/crypto/index.ts b/frontend/src/lib/crypto/index.ts index d0d6cd5dd..d722b68ea 100644 --- a/frontend/src/lib/crypto/index.ts +++ b/frontend/src/lib/crypto/index.ts @@ -4,53 +4,6 @@ import jsrp from "jsrp"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto"; -import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries"; - -export const generateUserBackupKey = async (email: string, password: string) => { - // eslint-disable-next-line new-cap - const clientKey = new jsrp.client(); - // eslint-disable-next-line new-cap - const clientPassword = new jsrp.client(); - - await new Promise((resolve) => { - clientPassword.init({ username: email, password }, () => resolve(null)); - }); - const clientPublicKey = clientPassword.getPublicKey(); - const srpKeys = await srp1({ clientPublicKey }); - clientPassword.setSalt(srpKeys.salt); - clientPassword.setServerPublicKey(srpKeys.serverPublicKey); - - const clientProof = clientPassword.getProof(); // called M1 - const generatedKey = crypto.randomBytes(16).toString("hex"); - - await new Promise((resolve) => { - clientKey.init({ username: email, password: generatedKey }, () => resolve(null)); - }); - - const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>( - (resolve, reject) => { - clientKey.createVerifier((err, res) => { - if (err) return reject(err); - return resolve(res); - }); - } - ); - const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ - text: String(localStorage.getItem("PRIVATE_KEY")), - secret: generatedKey - }); - - await issueBackupPrivateKey({ - encryptedPrivateKey: ciphertext, - iv, - tag, - salt, - verifier, - clientProof - }); - - return generatedKey; -}; export const generateUserPassKey = async ( email: string, diff --git a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx index 2d6aeb9d6..19c1537a2 100644 --- a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx +++ b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx @@ -8,7 +8,6 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; // TODO(akhilmhdh): rewrite this into module functions in lib -import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, ContentLoader, FormControl, Input } from "@app/components/v2"; import { useServerConfig } from "@app/context"; @@ -63,13 +62,6 @@ export const SignUpPage = () => { }); SecurityClient.setToken(res.token); - saveTokenToLocalStorage({ - publicKey: userPass.publicKey, - encryptedPrivateKey: userPass.encryptedPrivateKey, - iv: userPass.encryptedPrivateKeyIV, - tag: userPass.encryptedPrivateKeyTag, - privateKey - }); await selectOrganization({ organizationId: res.organization.id }); // TODO(akhilmhdh): This is such a confusing pattern and too unreliable diff --git a/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx b/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx index 86dff5288..9666a1af4 100644 --- a/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx @@ -18,7 +18,7 @@ import { useToggle } from "@app/hooks"; import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -import { fetchMyPrivateKey, fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries"; +import { fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries"; import { EmailDuplicationConfirmation } from "@app/pages/auth/SelectOrgPage/EmailDuplicationConfirmation"; import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils"; @@ -70,9 +70,6 @@ export const PasswordStep = ({ // set JWT token SecurityClient.setToken(oauthLogin.token); - const privateKey = await fetchMyPrivateKey(); - localStorage.setItem("PRIVATE_KEY", privateKey); - // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org if (organizationId) { const finishWithOrgWorkflow = async () => { @@ -92,7 +89,7 @@ export const PasswordStep = ({ console.log("organization id was present. new JWT token to be used in CLI:", token); const instance = axios.create(); const payload = { - privateKey, + privateKey: "", // note(daniel): no longer needed by the CLI, because the CLI only uses the private key to create service tokens, and the private key isn't used anymore when creating service tokens. email, JTWToken: token }; diff --git a/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx index 311d613ef..a134220e8 100644 --- a/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx +++ b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx @@ -1,22 +1,14 @@ -import crypto from "crypto"; - import { FormEvent, useState } from "react"; import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate, useSearch } from "@tanstack/react-router"; -import jsrp from "jsrp"; import { createNotification } from "@app/components/notifications"; import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck"; -import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { useSetupPassword } from "@app/hooks/api/auth/queries"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - export const PasswordSetupPage = () => { const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -65,83 +57,31 @@ export const PasswordSetupPage = () => { setPasswordsMatch(true); if (!errorCheck) { - client.init( - { - username: email, + try { + await setupPassword.mutateAsync({ + email, + token, password - }, - async () => { - client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); + }); - if (!derivedKey) throw new Error("Failed to derive key from password"); + setIsRedirecting(true); - const key = crypto.randomBytes(32); + createNotification({ + type: "success", + title: "Password successfully set", + text: "Redirecting to login..." + }); - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: localStorage.getItem("PRIVATE_KEY") as string, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - try { - await setupPassword.mutateAsync({ - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt: result.salt, - verifier: result.verifier, - token, - password - }); - - setIsRedirecting(true); - - createNotification({ - type: "success", - title: "Password successfully set", - text: "Redirecting to login..." - }); - - setTimeout(() => { - window.location.href = "/login"; - }, 3000); - } catch (error) { - createNotification({ - type: "error", - text: (error as Error).message ?? "Error setting password" - }); - navigate({ to: "/personal-settings" }); - } - }); - } - ); + setTimeout(() => { + window.location.href = "/login"; + }, 3000); + } catch (error) { + createNotification({ + type: "error", + text: (error as Error).message ?? "Error setting password" + }); + navigate({ to: "/personal-settings" }); + } } }; diff --git a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx index 3e3a5909f..f331e4bda 100644 --- a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx @@ -123,11 +123,8 @@ export const SelectOrganizationSection = () => { } if (callbackPort) { - const privateKey = localStorage.getItem("PRIVATE_KEY"); - let error: string | null = null; - if (!privateKey) error = "Private key not found"; if (!user?.email) error = "User email not found"; if (!token) error = "No token found"; @@ -142,7 +139,7 @@ export const SelectOrganizationSection = () => { const payload = { JTWToken: token, email: user?.email, - privateKey + privateKey: "" } as IsCliLoginSuccessful["loginResponse"]; // send request to server endpoint diff --git a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx index a4ebe7a11..e05e0f2ad 100644 --- a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx +++ b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx @@ -1,23 +1,14 @@ -/* eslint-disable no-nested-ternary */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import crypto from "crypto"; - import { useState } from "react"; import { Helmet } from "react-helmet"; import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useNavigate, useSearch } from "@tanstack/react-router"; -import jsrp from "jsrp"; import { Mfa } from "@app/components/auth/Mfa"; import InputField from "@app/components/basic/InputField"; import checkPassword from "@app/components/utilities/checks/password/checkPassword"; -import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto"; -import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button } from "@app/components/v2"; -import { useServerConfig } from "@app/context"; import { useToggle } from "@app/hooks"; import { completeAccountSignupInvite, @@ -28,9 +19,6 @@ import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { isLoggedIn } from "@app/hooks/api/reactQuery"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - type Errors = { tooShort?: string; tooLong?: string; @@ -67,7 +55,6 @@ export const SignupInvitePage = () => { const metadata = queryParams.get("metadata") || undefined; const { mutateAsync: selectOrganization } = useSelectOrganization(); - const { config } = useServerConfig(); const loggedIn = isLoggedIn(); @@ -94,123 +81,56 @@ export const SignupInvitePage = () => { } if (!errorCheck) { - // Generate a random pair of a public and a private key - const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled); + try { + const { token: jwtToken } = await completeAccountSignupInvite({ + email, + password, + firstName, + lastName, + tokenMetadata: metadata + }); - localStorage.setItem("PRIVATE_KEY", privateKey); + // unset temporary signup JWT token and set JWT token + SecurityClient.setSignupToken(""); + SecurityClient.setToken(jwtToken); - client.init( - { - username: email, - password - }, - async () => { - client.createVerifier(async (_err, result) => { - try { - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); + const userOrgs = await fetchOrganizations(); - if (!derivedKey) throw new Error("Failed to derive key from password"); + const orgId = userOrgs[0].id; - const key = crypto.randomBytes(32); + if (!orgId) throw new Error("You are not part of any organization"); - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: privateKey, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - const { token: jwtToken } = await completeAccountSignupInvite({ - email, - password, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt: result.salt, - verifier: result.verifier, - tokenMetadata: metadata - }); - - // unset temporary signup JWT token and set JWT token - SecurityClient.setSignupToken(""); - SecurityClient.setToken(jwtToken); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey - }); - - const userOrgs = await fetchOrganizations(); - - const orgId = userOrgs[0].id; - - if (!orgId) throw new Error("You are not part of any organization"); - - const completeSignupFlow = async () => { - const { - token: mfaToken, - isMfaEnabled, - mfaMethod - } = await selectOrganization({ - organizationId: orgId - }); - - if (isMfaEnabled) { - SecurityClient.setMfaToken(mfaToken); - if (mfaMethod) { - setRequiredMfaMethod(mfaMethod); - } - toggleShowMfa.on(); - setMfaSuccessCallback(() => completeSignupFlow); - return; - } - - localStorage.setItem("orgData.id", orgId); - - navigate({ - to: "/organization/projects" - }); - }; - - await completeSignupFlow(); - } catch (error) { - setIsLoading(false); - console.error(error); - } + const completeSignupFlow = async () => { + const { + token: mfaToken, + isMfaEnabled, + mfaMethod + } = await selectOrganization({ + organizationId: orgId }); - } - ); + + if (isMfaEnabled) { + SecurityClient.setMfaToken(mfaToken); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } + toggleShowMfa.on(); + setMfaSuccessCallback(() => completeSignupFlow); + return; + } + + localStorage.setItem("orgData.id", orgId); + + navigate({ + to: "/organization/projects" + }); + }; + + await completeSignupFlow(); + } catch (error) { + setIsLoading(false); + console.error(error); + } } else { setIsLoading(false); } diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 85343e79e..1b10ed94c 100644 --- a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -3,24 +3,16 @@ import crypto from "crypto"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "@tanstack/react-router"; -import jsrp from "jsrp"; import { Mfa } from "@app/components/auth/Mfa"; -import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto"; -import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, Input } from "@app/components/v2"; -import { useServerConfig } from "@app/context"; import { initProjectHelper } from "@app/helpers/project"; import { useToggle } from "@app/hooks"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - type Props = { username: string; password: string; @@ -64,7 +56,6 @@ export const UserInfoSSOStep = ({ const { mutateAsync: selectOrganization } = useSelectOrganization(); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const navigate = useNavigate(); - const { config } = useServerConfig(); useEffect(() => { const randomPassword = crypto.randomBytes(32).toString("hex"); @@ -93,131 +84,64 @@ export const UserInfoSSOStep = ({ } if (!errorCheck) { - // Generate a random pair of a public and a private key - const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled); - localStorage.setItem("PRIVATE_KEY", privateKey); + try { + const response = await completeAccountSignup({ + email: username, + password, + firstName: name.split(" ")[0], + lastName: name.split(" ").slice(1).join(" "), + providerAuthToken, + organizationName, + attributionSource, + useDefaultOrg: forceDefaultOrg + }); - client.init( - { - username, - password - }, - async () => { - client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { - try { - // TODO: moduralize into KeyService - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); + // unset signup JWT token and set JWT token + SecurityClient.setSignupToken(""); + SecurityClient.setToken(response.token); + SecurityClient.setProviderAuthToken(""); - if (!derivedKey) throw new Error("Failed to derive key from password"); + const userOrgs = await fetchOrganizations(); + const orgId = userOrgs[0]?.id; - const key = crypto.randomBytes(32); + const completeSignupFlow = async () => { + try { + const { isMfaEnabled, token, mfaMethod } = await selectOrganization({ + organizationId: orgId + }); - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: privateKey, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - const response = await completeAccountSignup({ - email: username, - password, - firstName: name.split(" ")[0], - lastName: name.split(" ").slice(1).join(" "), - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - providerAuthToken, - salt: result.salt, - verifier: result.verifier, - organizationName, - attributionSource, - useDefaultOrg: forceDefaultOrg - }); - - // unset signup JWT token and set JWT token - SecurityClient.setSignupToken(""); - SecurityClient.setToken(response.token); - SecurityClient.setProviderAuthToken(""); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey - }); - - const userOrgs = await fetchOrganizations(); - const orgId = userOrgs[0]?.id; - - const completeSignupFlow = async () => { - try { - const { isMfaEnabled, token, mfaMethod } = await selectOrganization({ - organizationId: orgId - }); - - if (isMfaEnabled) { - SecurityClient.setMfaToken(token); - if (mfaMethod) { - setRequiredMfaMethod(mfaMethod); - } - toggleShowMfa.on(); - setMfaSuccessCallback(() => completeSignupFlow); - return; - } - - // only create example project if not joining existing org - if (!providerOrganizationName) { - await initProjectHelper({ - projectName: "Example Project" - }); - } - - localStorage.setItem("orgData.id", orgId); - navigate({ - to: "/organization/projects" - }); - } catch (error) { - setIsLoading(false); - console.error(error); - } - }; - - await completeSignupFlow(); - } catch (error) { - setIsLoading(false); - console.error(error); + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } + toggleShowMfa.on(); + setMfaSuccessCallback(() => completeSignupFlow); + return; } - }); - } - ); + + // only create example project if not joining existing org + if (!providerOrganizationName) { + await initProjectHelper({ + projectName: "Example Project" + }); + } + + localStorage.setItem("orgData.id", orgId); + navigate({ + to: "/organization/projects" + }); + } catch (error) { + setIsLoading(false); + console.error(error); + } + }; + + await completeSignupFlow(); + } catch (error) { + setIsLoading(false); + console.error(error); + } } else { setIsLoading(false); } diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx new file mode 100644 index 000000000..3bd423982 --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx @@ -0,0 +1,317 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem, + TextArea +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + MachineIdentityAuthMethod, + useCreateIdentityAuthTemplate, + useUpdateIdentityAuthTemplate +} from "@app/hooks/api/identityAuthTemplates"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const authMethods = [{ label: "LDAP Auth", value: MachineIdentityAuthMethod.LDAP }]; + +const schema = z.object({ + name: z.string().min(1, "Template name is required"), + method: z.nativeEnum(MachineIdentityAuthMethod), + url: z.string().min(1, "LDAP URL is required"), + bindDN: z.string().min(1, "Bind DN is required"), + bindPass: z.string().min(1, "Bind Pass is required"), + searchBase: z.string().min(1, "Search Base / DN is required"), + ldapCaCertificate: z + .string() + .optional() + .transform((val) => val || undefined) +}); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["createTemplate", "editTemplate"]>; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["createTemplate", "editTemplate"]>, + state?: boolean + ) => void; +}; + +export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + + const { mutateAsync: createTemplate } = useCreateIdentityAuthTemplate(); + const { mutateAsync: updateTemplate } = useUpdateIdentityAuthTemplate(); + + const isEdit = popUp.editTemplate.isOpen; + const template = popUp.editTemplate?.data?.template; + + const { + control, + handleSubmit, + reset, + watch, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "", + method: MachineIdentityAuthMethod.LDAP, + url: "", + bindDN: "", + bindPass: "", + searchBase: "", + ldapCaCertificate: "" + } + }); + + useEffect(() => { + if (isEdit && template) { + reset({ + name: template.name || "", + method: MachineIdentityAuthMethod.LDAP, + url: template.templateFields?.url || "", + bindDN: template.templateFields?.bindDN || "", + bindPass: template.templateFields?.bindPass || "", + searchBase: template.templateFields?.searchBase || "", + ldapCaCertificate: template.templateFields?.ldapCaCertificate || "" + }); + } else { + reset({ + name: "", + method: MachineIdentityAuthMethod.LDAP, + url: "", + bindDN: "", + bindPass: "", + searchBase: "", + ldapCaCertificate: "" + }); + } + }, [isEdit, template, reset]); + + const selectedMethod = watch("method"); + + const onFormSubmit = async (data: FormData) => { + try { + if (isEdit && template) { + await updateTemplate({ + templateId: template.id, + organizationId: orgId, + name: data.name, + templateFields: { + url: data.url, + bindDN: data.bindDN, + bindPass: data.bindPass, + searchBase: data.searchBase, + ldapCaCertificate: data.ldapCaCertificate + } + }); + createNotification({ + text: "Successfully updated auth template", + type: "success" + }); + } else { + await createTemplate({ + organizationId: orgId, + name: data.name, + authMethod: data.method, + templateFields: { + url: data.url, + bindDN: data.bindDN, + bindPass: data.bindPass, + searchBase: data.searchBase, + ldapCaCertificate: data.ldapCaCertificate + } + }); + createNotification({ + text: "Successfully created auth template", + type: "success" + }); + } + + handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false); + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = + error?.response?.data?.message ?? `Failed to ${isEdit ? "update" : "create"} auth template`; + + createNotification({ + text, + type: "error" + }); + } + }; + + const handleClose = () => { + handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false); + reset(); + }; + + return ( + + +
+ ( + + + + )} + /> + + ( + + + + )} + /> + + {/* LDAP Configuration Fields */} + {selectedMethod === "ldap" && ( + <> + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + +