diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index 33360bf45..4597b64d9 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -171,6 +171,7 @@ ENV NODE_ENV production ENV STANDALONE_BUILD true ENV STANDALONE_MODE true ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ +ENV NODE_OPTIONS="--max-old-space-size=1024" WORKDIR /backend diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 6d582ce76..b21eaac09 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -168,6 +168,7 @@ ENV HTTPS_ENABLED false ENV NODE_ENV production ENV STANDALONE_BUILD true ENV STANDALONE_MODE true +ENV NODE_OPTIONS="--max-old-space-size=1024" WORKDIR /backend diff --git a/backend/e2e-test/mocks/keystore.ts b/backend/e2e-test/mocks/keystore.ts index 48f52f9e7..f4f251616 100644 --- a/backend/e2e-test/mocks/keystore.ts +++ b/backend/e2e-test/mocks/keystore.ts @@ -1,4 +1,8 @@ +import RE2 from "re2"; + import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { applyJitter } from "@app/lib/dates"; +import { delay as delayMs } from "@app/lib/delay"; import { Lock } from "@app/lib/red-lock"; export const mockKeyStore = (): TKeyStoreFactory => { @@ -18,6 +22,27 @@ export const mockKeyStore = (): TKeyStoreFactory => { delete store[key]; return 1; }, + deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => { + const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`); + let totalDeleted = 0; + const keys = Object.keys(store); + + for (let i = 0; i < keys.length; i += batchSize) { + const batch = keys.slice(i, i + batchSize); + + for (const key of batch) { + if (regex.test(key)) { + delete store[key]; + totalDeleted += 1; + } + } + + // eslint-disable-next-line no-await-in-loop + await delayMs(Math.max(0, applyJitter(delay, jitter))); + } + + return totalDeleted; + }, getItem: async (key) => { const value = store[key]; if (typeof value === "string") { diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 6ec542c6b..748a7d431 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -66,6 +66,8 @@ import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-a import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service"; import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; +import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service"; +import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; @@ -146,6 +148,13 @@ declare module "fastify" { providerAuthToken: string; externalProviderAccessToken?: string; }; + passportMachineIdentity: { + identityId: string; + user: { + uid: string; + mail?: string; + }; + }; kmipUser: { projectId: string; clientId: string; @@ -153,7 +162,9 @@ declare module "fastify" { }; auditLogInfo: Pick; ssoConfig: Awaited>; - ldapConfig: Awaited>; + ldapConfig: Awaited> & { + allowedFields?: TAllowedFields[]; + }; } interface FastifyInstance { @@ -199,6 +210,7 @@ declare module "fastify" { identityAzureAuth: TIdentityAzureAuthServiceFactory; identityOidcAuth: TIdentityOidcAuthServiceFactory; identityJwtAuth: TIdentityJwtAuthServiceFactory; + identityLdapAuth: TIdentityLdapAuthServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; accessApprovalRequest: TAccessApprovalRequestServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 13f3bc306..c26f1128e 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -432,6 +432,11 @@ import { TWorkflowIntegrationsInsert, TWorkflowIntegrationsUpdate } from "@app/db/schemas"; +import { + TIdentityLdapAuths, + TIdentityLdapAuthsInsert, + TIdentityLdapAuthsUpdate +} from "@app/db/schemas/identity-ldap-auths"; import { TMicrosoftTeamsIntegrations, TMicrosoftTeamsIntegrationsInsert, @@ -735,6 +740,11 @@ declare module "knex/types/tables" { TIdentityJwtAuthsInsert, TIdentityJwtAuthsUpdate >; + [TableName.IdentityLdapAuth]: KnexOriginal.CompositeTableType< + TIdentityLdapAuths, + TIdentityLdapAuthsInsert, + TIdentityLdapAuthsUpdate + >; [TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType< TIdentityUaClientSecrets, TIdentityUaClientSecretsInsert, diff --git a/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts b/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts new file mode 100644 index 000000000..f90b2d593 --- /dev/null +++ b/backend/src/db/migrations/20250429232917_store-cert-secret-key-and-chain.ts @@ -0,0 +1,33 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.CertificateBody, "encryptedCertificateChain"))) { + await knex.schema.alterTable(TableName.CertificateBody, (t) => { + t.binary("encryptedCertificateChain").nullable(); + }); + } + + if (!(await knex.schema.hasTable(TableName.CertificateSecret))) { + await knex.schema.createTable(TableName.CertificateSecret, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("certId").notNullable().unique(); + t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE"); + t.binary("encryptedPrivateKey").notNullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateSecret)) { + await knex.schema.dropTable(TableName.CertificateSecret); + } + + if (await knex.schema.hasColumn(TableName.CertificateBody, "encryptedCertificateChain")) { + await knex.schema.alterTable(TableName.CertificateBody, (t) => { + t.dropColumn("encryptedCertificateChain"); + }); + } +} diff --git a/backend/src/db/migrations/20250505203703_project-templates-type-col.ts b/backend/src/db/migrations/20250505203703_project-templates-type-col.ts new file mode 100644 index 000000000..d1ef14d72 --- /dev/null +++ b/backend/src/db/migrations/20250505203703_project-templates-type-col.ts @@ -0,0 +1,22 @@ +import { Knex } from "knex"; + +import { ProjectType, TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.ProjectTemplates, "type"))) { + await knex.schema.alterTable(TableName.ProjectTemplates, (t) => { + // defaulting to sm for migration to set existing, new ones will always be specified on creation + t.string("type").defaultTo(ProjectType.SecretManager).notNullable(); + t.jsonb("environments").nullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.ProjectTemplates, "type")) { + await knex.schema.alterTable(TableName.ProjectTemplates, (t) => { + t.dropColumn("type"); + // not reverting nullable environments + }); + } +} diff --git a/backend/src/db/migrations/20250507003056_identity-ldap-auth.ts b/backend/src/db/migrations/20250507003056_identity-ldap-auth.ts new file mode 100644 index 000000000..da9912022 --- /dev/null +++ b/backend/src/db/migrations/20250507003056_identity-ldap-auth.ts @@ -0,0 +1,39 @@ +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.IdentityLdapAuth))) { + await knex.schema.createTable(TableName.IdentityLdapAuth, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable(); + t.jsonb("accessTokenTrustedIps").notNullable(); + + t.uuid("identityId").notNullable().unique(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + + t.binary("encryptedBindDN").notNullable(); + t.binary("encryptedBindPass").notNullable(); + t.binary("encryptedLdapCaCertificate").nullable(); + + t.string("url").notNullable(); + t.string("searchBase").notNullable(); + t.string("searchFilter").notNullable(); + + t.jsonb("allowedFields").nullable(); + + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.IdentityLdapAuth); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityLdapAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityLdapAuth); +} diff --git a/backend/src/db/schemas/certificate-bodies.ts b/backend/src/db/schemas/certificate-bodies.ts index 75afbddbd..10171e383 100644 --- a/backend/src/db/schemas/certificate-bodies.ts +++ b/backend/src/db/schemas/certificate-bodies.ts @@ -14,7 +14,8 @@ export const CertificateBodiesSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), certId: z.string().uuid(), - encryptedCertificate: zodBuffer + encryptedCertificate: zodBuffer, + encryptedCertificateChain: zodBuffer.nullable().optional() }); export type TCertificateBodies = z.infer; diff --git a/backend/src/db/schemas/certificate-secrets.ts b/backend/src/db/schemas/certificate-secrets.ts index f8cad74f1..75e6377b2 100644 --- a/backend/src/db/schemas/certificate-secrets.ts +++ b/backend/src/db/schemas/certificate-secrets.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const CertificateSecretsSchema = z.object({ @@ -12,8 +14,7 @@ export const CertificateSecretsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), certId: z.string().uuid(), - pk: z.string(), - sk: z.string() + encryptedPrivateKey: zodBuffer }); export type TCertificateSecrets = z.infer; diff --git a/backend/src/db/schemas/identity-ldap-auths.ts b/backend/src/db/schemas/identity-ldap-auths.ts new file mode 100644 index 000000000..d5b15fc6a --- /dev/null +++ b/backend/src/db/schemas/identity-ldap-auths.ts @@ -0,0 +1,32 @@ +// 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 IdentityLdapAuthsSchema = z.object({ + id: z.string().uuid(), + accessTokenTTL: z.coerce.number().default(7200), + accessTokenMaxTTL: z.coerce.number().default(7200), + accessTokenNumUsesLimit: z.coerce.number().default(0), + accessTokenTrustedIps: z.unknown(), + identityId: z.string().uuid(), + encryptedBindDN: zodBuffer, + encryptedBindPass: zodBuffer, + encryptedLdapCaCertificate: zodBuffer.nullable().optional(), + url: z.string(), + searchBase: z.string(), + searchFilter: z.string(), + allowedFields: z.unknown().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TIdentityLdapAuths = z.infer; +export type TIdentityLdapAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityLdapAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 7fd77da6c..81d5319e1 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -80,6 +80,7 @@ export enum TableName { IdentityAwsAuth = "identity_aws_auths", IdentityOidcAuth = "identity_oidc_auths", IdentityJwtAuth = "identity_jwt_auths", + IdentityLdapAuth = "identity_ldap_auths", IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", @@ -185,11 +186,16 @@ export enum OrgMembershipStatus { } export enum ProjectMembershipRole { + // general Admin = "admin", Member = "member", Custom = "custom", Viewer = "viewer", - NoAccess = "no-access" + NoAccess = "no-access", + // ssh + SshHostBootstrapper = "ssh-host-bootstrapper", + // kms + KmsCryptographicOperator = "cryptographic-operator" } export enum SecretEncryptionAlgo { @@ -227,7 +233,8 @@ export enum IdentityAuthMethod { AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", OIDC_AUTH = "oidc-auth", - JWT_AUTH = "jwt-auth" + JWT_AUTH = "jwt-auth", + LDAP_AUTH = "ldap-auth" } export enum ProjectType { diff --git a/backend/src/db/schemas/project-templates.ts b/backend/src/db/schemas/project-templates.ts index 68f37d256..f12386165 100644 --- a/backend/src/db/schemas/project-templates.ts +++ b/backend/src/db/schemas/project-templates.ts @@ -12,10 +12,11 @@ export const ProjectTemplatesSchema = z.object({ name: z.string(), description: z.string().nullable().optional(), roles: z.unknown(), - environments: z.unknown(), + environments: z.unknown().nullable().optional(), orgId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + type: z.string().default("secret-manager") }); export type TProjectTemplates = z.infer; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 2403d6cf4..297601fd0 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -27,7 +27,7 @@ export const ProjectsSchema = z.object({ description: z.string().nullable().optional(), type: z.string(), enforceCapitalization: z.boolean().default(false), - hasDeleteProtection: z.boolean().default(true).nullable().optional() + hasDeleteProtection: z.boolean().default(false).nullable().optional() }); export type TProjects = z.infer; diff --git a/backend/src/ee/routes/v1/project-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index 08d16414b..5d33b4d58 100644 --- a/backend/src/ee/routes/v1/project-template-router.ts +++ b/backend/src/ee/routes/v1/project-template-router.ts @@ -1,9 +1,8 @@ import { z } from "zod"; -import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectTemplatesSchema, ProjectType } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { ApiDocsTags, ProjectTemplates } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -35,6 +34,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({ position: z.number().min(1) }) .array() + .nullable() }); const ProjectTemplateRolesSchema = z @@ -104,6 +104,9 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) hide: false, tags: [ApiDocsTags.ProjectTemplates], description: "List project templates for the current organization.", + querystring: z.object({ + type: z.nativeEnum(ProjectType).optional().describe(ProjectTemplates.LIST.type) + }), response: { 200: z.object({ projectTemplates: SanitizedProjectTemplateSchema.array() @@ -112,7 +115,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission); + const { type } = req.query; + const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission, type); const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name)); @@ -184,6 +188,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) tags: [ApiDocsTags.ProjectTemplates], description: "Create a project template.", body: z.object({ + type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type), name: slugSchema({ field: "name" }) .refine((val) => !isInfisicalProjectTemplate(val), { message: `The requested project template name is reserved.` @@ -191,9 +196,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) .describe(ProjectTemplates.CREATE.name), description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description), roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles), - environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe( - ProjectTemplates.CREATE.environments - ) + environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional() }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v1/secret-scanning-router.ts b/backend/src/ee/routes/v1/secret-scanning-router.ts index f144a6c00..1bc8e3998 100644 --- a/backend/src/ee/routes/v1/secret-scanning-router.ts +++ b/backend/src/ee/routes/v1/secret-scanning-router.ts @@ -1,11 +1,11 @@ import { z } from "zod"; import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas"; +import { canUseSecretScanning } from "@app/ee/services/secret-scanning/secret-scanning-fns"; import { SecretScanningResolvedStatus, SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types"; -import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { OrderByDirection } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -23,14 +23,14 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) = body: z.object({ organizationId: z.string().trim() }), response: { 200: z.object({ - sessionId: z.string() + sessionId: z.string(), + gitAppSlug: z.string() }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const appCfg = getConfig(); - if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) { + if (!canUseSecretScanning(req.auth.orgId)) { throw new BadRequestError({ message: "Secret scanning is temporarily unavailable." }); 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 1f4badfb5..a2caf2bff 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -34,6 +34,7 @@ import { WorkflowIntegration } from "@app/services/workflow-integration/workflow import { KmipPermission } from "../kmip/kmip-enum"; import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types"; +import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; export type TListProjectAuditLogDTO = { filter: { @@ -119,44 +120,60 @@ export enum EventType { CREATE_TOKEN_IDENTITY_TOKEN_AUTH = "create-token-identity-token-auth", UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth", + ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth", REVOKE_IDENTITY_TOKEN_AUTH = "revoke-identity-token-auth", + LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth", ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth", UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth", GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth", REVOKE_IDENTITY_KUBERNETES_AUTH = "revoke-identity-kubernetes-auth", + LOGIN_IDENTITY_OIDC_AUTH = "login-identity-oidc-auth", ADD_IDENTITY_OIDC_AUTH = "add-identity-oidc-auth", UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth", GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth", REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth", + LOGIN_IDENTITY_JWT_AUTH = "login-identity-jwt-auth", ADD_IDENTITY_JWT_AUTH = "add-identity-jwt-auth", UPDATE_IDENTITY_JWT_AUTH = "update-identity-jwt-auth", GET_IDENTITY_JWT_AUTH = "get-identity-jwt-auth", REVOKE_IDENTITY_JWT_AUTH = "revoke-identity-jwt-auth", + CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", + GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id", + LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth", ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth", UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth", REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth", GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth", + LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth", ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth", UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth", REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth", GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth", + LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth", ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth", UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth", GET_IDENTITY_AZURE_AUTH = "get-identity-azure-auth", REVOKE_IDENTITY_AZURE_AUTH = "revoke-identity-azure-auth", + + LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth", + ADD_IDENTITY_LDAP_AUTH = "add-identity-ldap-auth", + UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth", + GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth", + REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth", + CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", @@ -224,6 +241,8 @@ export enum EventType { DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", GET_CERT_BODY = "get-cert-body", + GET_CERT_PRIVATE_KEY = "get-cert-private-key", + GET_CERT_BUNDLE = "get-cert-bundle", CREATE_PKI_ALERT = "create-pki-alert", GET_PKI_ALERT = "get-pki-alert", UPDATE_PKI_ALERT = "update-pki-alert", @@ -1032,6 +1051,55 @@ interface GetIdentityAzureAuthEvent { }; } +interface LoginIdentityLdapAuthEvent { + type: EventType.LOGIN_IDENTITY_LDAP_AUTH; + metadata: { + identityId: string; + ldapUsername: string; + ldapEmail?: string; + }; +} + +interface AddIdentityLdapAuthEvent { + type: EventType.ADD_IDENTITY_LDAP_AUTH; + metadata: { + identityId: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + allowedFields?: TAllowedFields[]; + url: string; + }; +} + +interface UpdateIdentityLdapAuthEvent { + type: EventType.UPDATE_IDENTITY_LDAP_AUTH; + metadata: { + identityId: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + allowedFields?: TAllowedFields[]; + url?: string; + }; +} + +interface GetIdentityLdapAuthEvent { + type: EventType.GET_IDENTITY_LDAP_AUTH; + metadata: { + identityId: string; + }; +} + +interface RevokeIdentityLdapAuthEvent { + type: EventType.REVOKE_IDENTITY_LDAP_AUTH; + metadata: { + identityId: string; + }; +} + interface LoginIdentityOidcAuthEvent { type: EventType.LOGIN_IDENTITY_OIDC_AUTH; metadata: { @@ -1790,6 +1858,24 @@ interface GetCertBody { }; } +interface GetCertPrivateKey { + type: EventType.GET_CERT_PRIVATE_KEY; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + +interface GetCertBundle { + type: EventType.GET_CERT_BUNDLE; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + interface CreatePkiAlert { type: EventType.CREATE_PKI_ALERT; metadata: { @@ -2765,6 +2851,11 @@ export type Event = | UpdateIdentityJwtAuthEvent | GetIdentityJwtAuthEvent | DeleteIdentityJwtAuthEvent + | LoginIdentityLdapAuthEvent + | AddIdentityLdapAuthEvent + | UpdateIdentityLdapAuthEvent + | GetIdentityLdapAuthEvent + | RevokeIdentityLdapAuthEvent | CreateEnvironmentEvent | GetEnvironmentEvent | UpdateEnvironmentEvent @@ -2824,6 +2915,8 @@ export type Event = | DeleteCert | RevokeCert | GetCertBody + | GetCertPrivateKey + | GetCertBundle | CreatePkiAlert | GetPkiAlert | UpdatePkiAlert diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts index 05d492240..f653d0c0c 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-fns.ts @@ -24,8 +24,16 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) = if (net.isIPv4(el)) { exclusiveIps.push(el); } else { - const resolvedIps = await dns.resolve4(el); - exclusiveIps.push(...resolvedIps); + try { + const resolvedIps = await dns.resolve4(el); + exclusiveIps.push(...resolvedIps); + } catch (error) { + // only try lookup if not found + if ((error as { code: string })?.code !== "ENOTFOUND") throw error; + + const resolvedIps = (await dns.lookup(el, { all: true, family: 4 })).map(({ address }) => address); + exclusiveIps.push(...resolvedIps); + } } } } @@ -38,8 +46,16 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) = if (normalizedHost === "localhost" || normalizedHost === "host.docker.internal") { throw new BadRequestError({ message: "Invalid db host" }); } - const resolvedIps = await dns.resolve4(host); - inputHostIps.push(...resolvedIps); + try { + const resolvedIps = await dns.resolve4(host); + inputHostIps.push(...resolvedIps); + } catch (error) { + // only try lookup if not found + if ((error as { code: string })?.code !== "ENOTFOUND") throw error; + + const resolvedIps = (await dns.lookup(host, { all: true, family: 4 })).map(({ address }) => address); + inputHostIps.push(...resolvedIps); + } } if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) { diff --git a/backend/src/ee/services/ldap-config/ldap-config-types.ts b/backend/src/ee/services/ldap-config/ldap-config-types.ts index 86f4bf0d5..941335fa4 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-types.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-types.ts @@ -14,6 +14,11 @@ export type TLDAPConfig = { caCert: string; }; +export type TTestLDAPConfigDTO = Omit< + TLDAPConfig, + "organization" | "id" | "groupSearchBase" | "groupSearchFilter" | "isActive" | "uniqueUserAttribute" | "searchBase" +>; + export type TCreateLdapCfgDTO = { orgId: string; isActive: boolean; diff --git a/backend/src/ee/services/ldap-config/ldap-fns.ts b/backend/src/ee/services/ldap-config/ldap-fns.ts index 44af718ed..01b70b4db 100644 --- a/backend/src/ee/services/ldap-config/ldap-fns.ts +++ b/backend/src/ee/services/ldap-config/ldap-fns.ts @@ -2,15 +2,14 @@ import ldapjs from "ldapjs"; import { logger } from "@app/lib/logger"; -import { TLDAPConfig } from "./ldap-config-types"; +import { TLDAPConfig, TTestLDAPConfigDTO } from "./ldap-config-types"; export const isValidLdapFilter = (filter: string) => { try { ldapjs.parseFilter(filter); return true; } catch (error) { - logger.error("Invalid LDAP filter"); - logger.error(error); + logger.error(error, "Invalid LDAP filter"); return false; } }; @@ -20,7 +19,7 @@ export const isValidLdapFilter = (filter: string) => { * @param ldapConfig - The LDAP configuration to test * @returns {Boolean} isConnected - Whether or not the connection was successful */ -export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise => { +export const testLDAPConfig = async (ldapConfig: TTestLDAPConfigDTO): Promise => { return new Promise((resolve) => { const ldapClient = ldapjs.createClient({ url: ldapConfig.url, diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts new file mode 100644 index 000000000..44fcb7825 --- /dev/null +++ b/backend/src/ee/services/permission/default-roles.ts @@ -0,0 +1,448 @@ +import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"; + +import { + ProjectPermissionActions, + ProjectPermissionCertificateActions, + ProjectPermissionCmekActions, + ProjectPermissionDynamicSecretActions, + ProjectPermissionGroupActions, + ProjectPermissionIdentityActions, + ProjectPermissionKmipActions, + ProjectPermissionMemberActions, + ProjectPermissionSecretActions, + ProjectPermissionSecretRotationActions, + ProjectPermissionSecretSyncActions, + ProjectPermissionSet, + ProjectPermissionSshHostActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; + +const buildAdminPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); + + // Admins get full access to everything + [ + ProjectPermissionSub.SecretFolders, + ProjectPermissionSub.SecretImports, + ProjectPermissionSub.SecretApproval, + ProjectPermissionSub.Role, + ProjectPermissionSub.Integrations, + ProjectPermissionSub.Webhooks, + ProjectPermissionSub.ServiceTokens, + ProjectPermissionSub.Settings, + ProjectPermissionSub.Environments, + ProjectPermissionSub.Tags, + ProjectPermissionSub.AuditLogs, + ProjectPermissionSub.IpAllowList, + ProjectPermissionSub.CertificateAuthorities, + ProjectPermissionSub.CertificateTemplates, + ProjectPermissionSub.PkiAlerts, + ProjectPermissionSub.PkiCollections, + ProjectPermissionSub.SshCertificateAuthorities, + ProjectPermissionSub.SshCertificates, + ProjectPermissionSub.SshCertificateTemplates, + ProjectPermissionSub.SshHostGroups + ].forEach((el) => { + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + el + ); + }); + + can( + [ + ProjectPermissionCertificateActions.Read, + ProjectPermissionCertificateActions.Edit, + ProjectPermissionCertificateActions.Create, + ProjectPermissionCertificateActions.Delete, + ProjectPermissionCertificateActions.ReadPrivateKey + ], + ProjectPermissionSub.Certificates + ); + + can( + [ + ProjectPermissionSshHostActions.Edit, + ProjectPermissionSshHostActions.Read, + ProjectPermissionSshHostActions.Create, + ProjectPermissionSshHostActions.Delete, + ProjectPermissionSshHostActions.IssueHostCert + ], + ProjectPermissionSub.SshHosts + ); + + can( + [ + ProjectPermissionMemberActions.Create, + ProjectPermissionMemberActions.Edit, + ProjectPermissionMemberActions.Delete, + ProjectPermissionMemberActions.Read, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionMemberActions.AssumePrivileges + ], + ProjectPermissionSub.Member + ); + + can( + [ + ProjectPermissionGroupActions.Create, + ProjectPermissionGroupActions.Edit, + ProjectPermissionGroupActions.Delete, + ProjectPermissionGroupActions.Read, + ProjectPermissionGroupActions.GrantPrivileges + ], + ProjectPermissionSub.Groups + ); + + can( + [ + ProjectPermissionIdentityActions.Create, + ProjectPermissionIdentityActions.Edit, + ProjectPermissionIdentityActions.Delete, + ProjectPermissionIdentityActions.Read, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionIdentityActions.AssumePrivileges + ], + ProjectPermissionSub.Identity + ); + + can( + [ + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Delete + ], + ProjectPermissionSub.Secrets + ); + + can( + [ + ProjectPermissionDynamicSecretActions.ReadRootCredential, + ProjectPermissionDynamicSecretActions.EditRootCredential, + ProjectPermissionDynamicSecretActions.CreateRootCredential, + ProjectPermissionDynamicSecretActions.DeleteRootCredential, + ProjectPermissionDynamicSecretActions.Lease + ], + ProjectPermissionSub.DynamicSecrets + ); + + can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project); + can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); + can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms); + can( + [ + ProjectPermissionCmekActions.Create, + ProjectPermissionCmekActions.Edit, + ProjectPermissionCmekActions.Delete, + ProjectPermissionCmekActions.Read, + ProjectPermissionCmekActions.Encrypt, + ProjectPermissionCmekActions.Decrypt, + ProjectPermissionCmekActions.Sign, + ProjectPermissionCmekActions.Verify + ], + ProjectPermissionSub.Cmek + ); + can( + [ + ProjectPermissionSecretSyncActions.Create, + ProjectPermissionSecretSyncActions.Edit, + ProjectPermissionSecretSyncActions.Delete, + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSecretSyncActions.SyncSecrets, + ProjectPermissionSecretSyncActions.ImportSecrets, + ProjectPermissionSecretSyncActions.RemoveSecrets + ], + ProjectPermissionSub.SecretSyncs + ); + + can( + [ + ProjectPermissionKmipActions.CreateClients, + ProjectPermissionKmipActions.UpdateClients, + ProjectPermissionKmipActions.DeleteClients, + ProjectPermissionKmipActions.ReadClients, + ProjectPermissionKmipActions.GenerateClientCertificates + ], + ProjectPermissionSub.Kmip + ); + + can( + [ + ProjectPermissionSecretRotationActions.Create, + ProjectPermissionSecretRotationActions.Edit, + ProjectPermissionSecretRotationActions.Delete, + ProjectPermissionSecretRotationActions.Read, + ProjectPermissionSecretRotationActions.ReadGeneratedCredentials, + ProjectPermissionSecretRotationActions.RotateSecrets + ], + ProjectPermissionSub.SecretRotation + ); + + return rules; +}; + +const buildMemberPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); + + can( + [ + ProjectPermissionSecretActions.DescribeAndReadValue, + ProjectPermissionSecretActions.DescribeSecret, + ProjectPermissionSecretActions.ReadValue, + ProjectPermissionSecretActions.Edit, + ProjectPermissionSecretActions.Create, + ProjectPermissionSecretActions.Delete + ], + ProjectPermissionSub.Secrets + ); + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.SecretFolders + ); + can( + [ + ProjectPermissionDynamicSecretActions.ReadRootCredential, + ProjectPermissionDynamicSecretActions.EditRootCredential, + ProjectPermissionDynamicSecretActions.CreateRootCredential, + ProjectPermissionDynamicSecretActions.DeleteRootCredential, + ProjectPermissionDynamicSecretActions.Lease + ], + ProjectPermissionSub.DynamicSecrets + ); + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.SecretImports + ); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval); + can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation); + + can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); + + can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member); + + can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.Integrations + ); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.Webhooks + ); + + can( + [ + ProjectPermissionIdentityActions.Read, + ProjectPermissionIdentityActions.Edit, + ProjectPermissionIdentityActions.Create, + ProjectPermissionIdentityActions.Delete + ], + ProjectPermissionSub.Identity + ); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.ServiceTokens + ); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.Settings + ); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.Environments + ); + + can( + [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Edit, + ProjectPermissionActions.Create, + ProjectPermissionActions.Delete + ], + ProjectPermissionSub.Tags + ); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.Role); + can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs); + can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList); + + // double check if all CRUD are needed for CA and Certificates + can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities); + + can( + [ + ProjectPermissionCertificateActions.Read, + ProjectPermissionCertificateActions.Edit, + ProjectPermissionCertificateActions.Create, + ProjectPermissionCertificateActions.Delete + ], + ProjectPermissionSub.Certificates + ); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); + can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); + + can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates); + can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates); + can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); + + can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts); + + can( + [ + ProjectPermissionCmekActions.Create, + ProjectPermissionCmekActions.Edit, + ProjectPermissionCmekActions.Delete, + ProjectPermissionCmekActions.Read, + ProjectPermissionCmekActions.Encrypt, + ProjectPermissionCmekActions.Decrypt, + ProjectPermissionCmekActions.Sign, + ProjectPermissionCmekActions.Verify + ], + ProjectPermissionSub.Cmek + ); + + can( + [ + ProjectPermissionSecretSyncActions.Create, + ProjectPermissionSecretSyncActions.Edit, + ProjectPermissionSecretSyncActions.Delete, + ProjectPermissionSecretSyncActions.Read, + ProjectPermissionSecretSyncActions.SyncSecrets, + ProjectPermissionSecretSyncActions.ImportSecrets, + ProjectPermissionSecretSyncActions.RemoveSecrets + ], + ProjectPermissionSub.SecretSyncs + ); + + return rules; +}; + +const buildViewerPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); + + can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); + can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); + can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); + can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation); + can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); + can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity); + can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); + can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); + can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); + can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates); + can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); + can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); + can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs); + + return rules; +}; + +const buildNoAccessProjectPermission = () => { + const { rules } = new AbilityBuilder>(createMongoAbility); + return rules; +}; + +const buildSshHostBootstrapPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); + + can( + [ProjectPermissionSshHostActions.Create, ProjectPermissionSshHostActions.IssueHostCert], + ProjectPermissionSub.SshHosts + ); + + return rules; +}; + +const buildCryptographicOperatorPermissionRules = () => { + const { can, rules } = new AbilityBuilder>(createMongoAbility); + + can( + [ + ProjectPermissionCmekActions.Encrypt, + ProjectPermissionCmekActions.Decrypt, + ProjectPermissionCmekActions.Sign, + ProjectPermissionCmekActions.Verify + ], + ProjectPermissionSub.Cmek + ); + + return rules; +}; + +// General +export const projectAdminPermissions = buildAdminPermissionRules(); +export const projectMemberPermissions = buildMemberPermissionRules(); +export const projectViewerPermission = buildViewerPermissionRules(); +export const projectNoAccessPermissions = buildNoAccessProjectPermission(); + +// SSH +export const sshHostBootstrapPermissions = buildSshHostBootstrapPermissionRules(); + +// KMS +export const cryptographicOperatorPermissions = buildCryptographicOperatorPermissionRules(); diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 40988ac80..a1acaeb21 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -12,6 +12,14 @@ import { TIdentityProjectMemberships, TProjectMemberships } from "@app/db/schemas"; +import { + cryptographicOperatorPermissions, + projectAdminPermissions, + projectMemberPermissions, + projectNoAccessPermissions, + projectViewerPermission, + sshHostBootstrapPermissions +} from "@app/ee/services/permission/default-roles"; import { conditionsMatcher } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { objectify } from "@app/lib/fn"; @@ -32,14 +40,7 @@ import { TGetServiceTokenProjectPermissionArg, TGetUserProjectPermissionArg } from "./permission-service-types"; -import { - buildServiceTokenProjectPermission, - projectAdminPermissions, - projectMemberPermissions, - projectNoAccessPermissions, - ProjectPermissionSet, - projectViewerPermission -} from "./project-permission"; +import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission"; type TPermissionServiceFactoryDep = { orgRoleDAL: Pick; @@ -95,6 +96,10 @@ export const permissionServiceFactory = ({ return projectViewerPermission; case ProjectMembershipRole.NoAccess: return projectNoAccessPermissions; + case ProjectMembershipRole.SshHostBootstrapper: + return sshHostBootstrapPermissions; + case ProjectMembershipRole.KmsCryptographicOperator: + return cryptographicOperatorPermissions; case ProjectMembershipRole.Custom: { return unpackRules>>( permissions as PackRule>>[] diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 319a0259a..7463c35f6 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -17,6 +17,14 @@ export enum ProjectPermissionActions { Delete = "delete" } +export enum ProjectPermissionCertificateActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ReadPrivateKey = "read-private-key" +} + export enum ProjectPermissionSecretActions { DescribeAndReadValue = "read", DescribeSecret = "describeSecret", @@ -232,7 +240,7 @@ export type ProjectPermissionSet = ProjectPermissionSub.Identity | (ForcedSubject & IdentityManagementSubjectFields) ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] - | [ProjectPermissionActions, ProjectPermissionSub.Certificates] + | [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] @@ -478,7 +486,7 @@ const GeneralPermissionSchema = [ }), z.object({ subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateActions).describe( "Describe what action an entity can take." ) }), @@ -670,393 +678,6 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ export type TProjectPermissionV2Schema = z.infer; -const buildAdminPermissionRules = () => { - const { can, rules } = new AbilityBuilder>(createMongoAbility); - - // Admins get full access to everything - [ - ProjectPermissionSub.SecretFolders, - ProjectPermissionSub.SecretImports, - ProjectPermissionSub.SecretApproval, - ProjectPermissionSub.Role, - ProjectPermissionSub.Integrations, - ProjectPermissionSub.Webhooks, - ProjectPermissionSub.ServiceTokens, - ProjectPermissionSub.Settings, - ProjectPermissionSub.Environments, - ProjectPermissionSub.Tags, - ProjectPermissionSub.AuditLogs, - ProjectPermissionSub.IpAllowList, - ProjectPermissionSub.CertificateAuthorities, - ProjectPermissionSub.Certificates, - ProjectPermissionSub.CertificateTemplates, - ProjectPermissionSub.PkiAlerts, - ProjectPermissionSub.PkiCollections, - ProjectPermissionSub.SshCertificateAuthorities, - ProjectPermissionSub.SshCertificates, - ProjectPermissionSub.SshCertificateTemplates, - ProjectPermissionSub.SshHostGroups - ].forEach((el) => { - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - el - ); - }); - - can( - [ - ProjectPermissionSshHostActions.Edit, - ProjectPermissionSshHostActions.Read, - ProjectPermissionSshHostActions.Create, - ProjectPermissionSshHostActions.Delete, - ProjectPermissionSshHostActions.IssueHostCert - ], - ProjectPermissionSub.SshHosts - ); - - can( - [ - ProjectPermissionMemberActions.Create, - ProjectPermissionMemberActions.Edit, - ProjectPermissionMemberActions.Delete, - ProjectPermissionMemberActions.Read, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionMemberActions.AssumePrivileges - ], - ProjectPermissionSub.Member - ); - - can( - [ - ProjectPermissionGroupActions.Create, - ProjectPermissionGroupActions.Edit, - ProjectPermissionGroupActions.Delete, - ProjectPermissionGroupActions.Read, - ProjectPermissionGroupActions.GrantPrivileges - ], - ProjectPermissionSub.Groups - ); - - can( - [ - ProjectPermissionIdentityActions.Create, - ProjectPermissionIdentityActions.Edit, - ProjectPermissionIdentityActions.Delete, - ProjectPermissionIdentityActions.Read, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionIdentityActions.AssumePrivileges - ], - ProjectPermissionSub.Identity - ); - - can( - [ - ProjectPermissionSecretActions.DescribeAndReadValue, - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSecretActions.ReadValue, - ProjectPermissionSecretActions.Create, - ProjectPermissionSecretActions.Edit, - ProjectPermissionSecretActions.Delete - ], - ProjectPermissionSub.Secrets - ); - - can( - [ - ProjectPermissionDynamicSecretActions.ReadRootCredential, - ProjectPermissionDynamicSecretActions.EditRootCredential, - ProjectPermissionDynamicSecretActions.CreateRootCredential, - ProjectPermissionDynamicSecretActions.DeleteRootCredential, - ProjectPermissionDynamicSecretActions.Lease - ], - ProjectPermissionSub.DynamicSecrets - ); - - can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project); - can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); - can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms); - can( - [ - ProjectPermissionCmekActions.Create, - ProjectPermissionCmekActions.Edit, - ProjectPermissionCmekActions.Delete, - ProjectPermissionCmekActions.Read, - ProjectPermissionCmekActions.Encrypt, - ProjectPermissionCmekActions.Decrypt, - ProjectPermissionCmekActions.Sign, - ProjectPermissionCmekActions.Verify - ], - ProjectPermissionSub.Cmek - ); - can( - [ - ProjectPermissionSecretSyncActions.Create, - ProjectPermissionSecretSyncActions.Edit, - ProjectPermissionSecretSyncActions.Delete, - ProjectPermissionSecretSyncActions.Read, - ProjectPermissionSecretSyncActions.SyncSecrets, - ProjectPermissionSecretSyncActions.ImportSecrets, - ProjectPermissionSecretSyncActions.RemoveSecrets - ], - ProjectPermissionSub.SecretSyncs - ); - - can( - [ - ProjectPermissionKmipActions.CreateClients, - ProjectPermissionKmipActions.UpdateClients, - ProjectPermissionKmipActions.DeleteClients, - ProjectPermissionKmipActions.ReadClients, - ProjectPermissionKmipActions.GenerateClientCertificates - ], - ProjectPermissionSub.Kmip - ); - - can( - [ - ProjectPermissionSecretRotationActions.Create, - ProjectPermissionSecretRotationActions.Edit, - ProjectPermissionSecretRotationActions.Delete, - ProjectPermissionSecretRotationActions.Read, - ProjectPermissionSecretRotationActions.ReadGeneratedCredentials, - ProjectPermissionSecretRotationActions.RotateSecrets - ], - ProjectPermissionSub.SecretRotation - ); - - return rules; -}; - -export const projectAdminPermissions = buildAdminPermissionRules(); - -const buildMemberPermissionRules = () => { - const { can, rules } = new AbilityBuilder>(createMongoAbility); - - can( - [ - ProjectPermissionSecretActions.DescribeAndReadValue, - ProjectPermissionSecretActions.DescribeSecret, - ProjectPermissionSecretActions.ReadValue, - ProjectPermissionSecretActions.Edit, - ProjectPermissionSecretActions.Create, - ProjectPermissionSecretActions.Delete - ], - ProjectPermissionSub.Secrets - ); - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.SecretFolders - ); - can( - [ - ProjectPermissionDynamicSecretActions.ReadRootCredential, - ProjectPermissionDynamicSecretActions.EditRootCredential, - ProjectPermissionDynamicSecretActions.CreateRootCredential, - ProjectPermissionDynamicSecretActions.DeleteRootCredential, - ProjectPermissionDynamicSecretActions.Lease - ], - ProjectPermissionSub.DynamicSecrets - ); - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.SecretImports - ); - - can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval); - can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation); - - can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); - - can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member); - - can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Integrations - ); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Webhooks - ); - - can( - [ - ProjectPermissionIdentityActions.Read, - ProjectPermissionIdentityActions.Edit, - ProjectPermissionIdentityActions.Create, - ProjectPermissionIdentityActions.Delete - ], - ProjectPermissionSub.Identity - ); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.ServiceTokens - ); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Settings - ); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Environments - ); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Tags - ); - - can([ProjectPermissionActions.Read], ProjectPermissionSub.Role); - can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs); - can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList); - - // double check if all CRUD are needed for CA and Certificates - can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities); - - can( - [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete - ], - ProjectPermissionSub.Certificates - ); - - can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates); - - can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); - can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); - - can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates); - can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates); - can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); - - can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts); - - can( - [ - ProjectPermissionCmekActions.Create, - ProjectPermissionCmekActions.Edit, - ProjectPermissionCmekActions.Delete, - ProjectPermissionCmekActions.Read, - ProjectPermissionCmekActions.Encrypt, - ProjectPermissionCmekActions.Decrypt, - ProjectPermissionCmekActions.Sign, - ProjectPermissionCmekActions.Verify - ], - ProjectPermissionSub.Cmek - ); - - can( - [ - ProjectPermissionSecretSyncActions.Create, - ProjectPermissionSecretSyncActions.Edit, - ProjectPermissionSecretSyncActions.Delete, - ProjectPermissionSecretSyncActions.Read, - ProjectPermissionSecretSyncActions.SyncSecrets, - ProjectPermissionSecretSyncActions.ImportSecrets, - ProjectPermissionSecretSyncActions.RemoveSecrets - ], - ProjectPermissionSub.SecretSyncs - ); - - return rules; -}; - -export const projectMemberPermissions = buildMemberPermissionRules(); - -const buildViewerPermissionRules = () => { - const { can, rules } = new AbilityBuilder>(createMongoAbility); - - can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); - can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); - can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); - can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation); - can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); - can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); - can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity); - can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); - can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); - can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); - can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); - can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); - can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs); - - return rules; -}; - -export const projectViewerPermission = buildViewerPermissionRules(); - -const buildNoAccessProjectPermission = () => { - const { rules } = new AbilityBuilder>(createMongoAbility); - return rules; -}; - export const buildServiceTokenProjectPermission = ( scopes: Array<{ secretPath: string; environment: string }>, permission: string[] @@ -1098,8 +719,6 @@ export const buildServiceTokenProjectPermission = ( return build({ conditionsMatcher }); }; -export const projectNoAccessPermissions = buildNoAccessProjectPermission(); - /* eslint-disable */ /** diff --git a/backend/src/ee/services/project-template/project-template-fns.ts b/backend/src/ee/services/project-template/project-template-fns.ts index 2ca78e876..8e8ebfa13 100644 --- a/backend/src/ee/services/project-template/project-template-fns.ts +++ b/backend/src/ee/services/project-template/project-template-fns.ts @@ -1,22 +1,27 @@ -import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; +import { ProjectType } from "@app/db/schemas"; import { InfisicalProjectTemplate, TUnpackedPermission } from "@app/ee/services/project-template/project-template-types"; import { getPredefinedRoles } from "@app/services/project-role/project-role-fns"; -export const getDefaultProjectTemplate = (orgId: string) => ({ +import { ProjectTemplateDefaultEnvironments } from "./project-template-constants"; + +export const getDefaultProjectTemplate = (orgId: string, type: ProjectType) => ({ id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod + type, name: InfisicalProjectTemplate.Default, createdAt: new Date(), updatedAt: new Date(), - description: "Infisical's default project template", - environments: ProjectTemplateDefaultEnvironments, - roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({ - name, - slug, - permissions: permissions as TUnpackedPermission[] - })), + description: `Infisical's ${type} default project template`, + environments: type === ProjectType.SecretManager ? ProjectTemplateDefaultEnvironments : null, + roles: [...getPredefinedRoles({ projectId: "project-template", projectType: type })].map( + ({ name, slug, permissions }) => ({ + name, + slug, + permissions: permissions as TUnpackedPermission[] + }) + ), orgId }); diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index b2430ac14..5b6163977 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -1,10 +1,11 @@ import { ForbiddenError } from "@casl/ability"; import { packRules } from "@casl/ability/extra"; -import { TProjectTemplates } from "@app/db/schemas"; +import { ProjectType, TProjectTemplates } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { TCreateProjectTemplateDTO, @@ -32,11 +33,13 @@ const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTempla ...rest, environments: environments as TProjectTemplateEnvironment[], roles: [ - ...getPredefinedRoles("project-template").map(({ name, slug, permissions }) => ({ - name, - slug, - permissions: permissions as TUnpackedPermission[] - })), + ...getPredefinedRoles({ projectId: "project-template", projectType: rest.type as ProjectType }).map( + ({ name, slug, permissions }) => ({ + name, + slug, + permissions: permissions as TUnpackedPermission[] + }) + ), ...(roles as TProjectTemplateRole[]).map((role) => ({ ...role, permissions: unpackPermissions(role.permissions) @@ -49,7 +52,7 @@ export const projectTemplateServiceFactory = ({ permissionService, projectTemplateDAL }: TProjectTemplatesServiceFactoryDep) => { - const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => { + const listProjectTemplatesByOrg = async (actor: OrgServiceActor, type?: ProjectType) => { const plan = await licenseService.getPlan(actor.orgId); if (!plan.projectTemplates) @@ -68,11 +71,14 @@ export const projectTemplateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); const projectTemplates = await projectTemplateDAL.find({ - orgId: actor.orgId + orgId: actor.orgId, + ...(type ? { type } : {}) }); return [ - getDefaultProjectTemplate(actor.orgId), + ...(type + ? [getDefaultProjectTemplate(actor.orgId, type)] + : Object.values(ProjectType).map((projectType) => getDefaultProjectTemplate(actor.orgId, projectType))), ...projectTemplates.map((template) => $unpackProjectTemplate(template)) ]; }; @@ -134,7 +140,7 @@ export const projectTemplateServiceFactory = ({ }; const createProjectTemplate = async ( - { roles, environments, ...params }: TCreateProjectTemplateDTO, + { roles, environments, type, ...params }: TCreateProjectTemplateDTO, actor: OrgServiceActor ) => { const plan = await licenseService.getPlan(actor.orgId); @@ -154,6 +160,17 @@ export const projectTemplateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates); + if (environments && type !== ProjectType.SecretManager) { + throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" }); + } + + if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) { + throw new BadRequestError({ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + message: `Failed to create project template due to environment count exceeding your current limit of ${plan.environmentLimit}. Contact Infisical to increase limit.` + }); + } + const isConflictingName = Boolean( await projectTemplateDAL.findOne({ name: params.name, @@ -169,8 +186,10 @@ export const projectTemplateServiceFactory = ({ const projectTemplate = await projectTemplateDAL.create({ ...params, roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))), - environments: JSON.stringify(environments), - orgId: actor.orgId + environments: + type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null, + orgId: actor.orgId, + type }); return $unpackProjectTemplate(projectTemplate); @@ -202,6 +221,19 @@ export const projectTemplateServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates); + if (projectTemplate.type !== ProjectType.SecretManager && environments) + throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" }); + + if (projectTemplate.type === ProjectType.SecretManager && environments === null) + throw new BadRequestError({ message: "Environments cannot be removed for SecretManager project templates" }); + + if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) { + throw new BadRequestError({ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + message: `Failed to update project template due to environment count exceeding your current limit of ${plan.environmentLimit}. Contact Infisical to increase limit.` + }); + } + if (params.name && projectTemplate.name !== params.name) { const isConflictingName = Boolean( await projectTemplateDAL.findOne({ diff --git a/backend/src/ee/services/project-template/project-template-types.ts b/backend/src/ee/services/project-template/project-template-types.ts index c2764dc53..d53b2375e 100644 --- a/backend/src/ee/services/project-template/project-template-types.ts +++ b/backend/src/ee/services/project-template/project-template-types.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { TProjectEnvironments } from "@app/db/schemas"; +import { ProjectType, TProjectEnvironments } from "@app/db/schemas"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; @@ -15,8 +15,9 @@ export type TProjectTemplateRole = { export type TCreateProjectTemplateDTO = { name: string; description?: string; + type: ProjectType; roles: TProjectTemplateRole[]; - environments: TProjectTemplateEnvironment[]; + environments?: TProjectTemplateEnvironment[] | null; }; export type TUpdateProjectTemplateDTO = Partial; diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-fns.ts b/backend/src/ee/services/secret-scanning/secret-scanning-fns.ts new file mode 100644 index 000000000..b1e2e0bbb --- /dev/null +++ b/backend/src/ee/services/secret-scanning/secret-scanning-fns.ts @@ -0,0 +1,11 @@ +import { getConfig } from "@app/lib/config/env"; + +export const canUseSecretScanning = (orgId: string) => { + const appCfg = getConfig(); + + if (!appCfg.isCloud) { + return true; + } + + return appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(orgId); +}; diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts index c5e7be9d8..7d41091fc 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts @@ -12,6 +12,7 @@ import { NotFoundError } from "@app/lib/errors"; import { TGitAppDALFactory } from "./git-app-dal"; import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal"; import { TSecretScanningDALFactory } from "./secret-scanning-dal"; +import { canUseSecretScanning } from "./secret-scanning-fns"; import { TSecretScanningQueueFactory } from "./secret-scanning-queue"; import { SecretScanningRiskStatus, @@ -47,12 +48,14 @@ export const secretScanningServiceFactory = ({ actorAuthMethod, actorOrgId }: TInstallAppSessionDTO) => { + const appCfg = getConfig(); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); const sessionId = crypto.randomBytes(16).toString("hex"); await gitAppInstallSessionDAL.upsert({ orgId, sessionId, userId: actorId }); - return { sessionId }; + return { sessionId, gitAppSlug: appCfg.SECRET_SCANNING_GIT_APP_SLUG }; }; const linkInstallationToOrg = async ({ @@ -91,7 +94,8 @@ export const secretScanningServiceFactory = ({ const { data: { repositories } } = await octokit.apps.listReposAccessibleToInstallation(); - if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) { + + if (canUseSecretScanning(actorOrgId)) { await Promise.all( repositories.map(({ id, full_name }) => secretScanningQueue.startFullRepoScan({ @@ -102,6 +106,7 @@ export const secretScanningServiceFactory = ({ ) ); } + return { installatedApp }; }; @@ -164,7 +169,6 @@ export const secretScanningServiceFactory = ({ }; const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => { - const appCfg = getConfig(); const { commits, repository, installation, pusher } = payload; if (!commits || !repository || !installation || !pusher) { return; @@ -175,7 +179,7 @@ export const secretScanningServiceFactory = ({ }); if (!installationLink) return; - if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) { + if (canUseSecretScanning(installationLink.orgId)) { await secretScanningQueue.startPushEventScan({ commits, pusher: { name: pusher.name, email: pusher.email }, diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index ac28e9ade..6da6c4fa4 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -1,6 +1,8 @@ import { Redis } from "ioredis"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; +import { applyJitter } from "@app/lib/dates"; +import { delay as delayMs } from "@app/lib/delay"; import { Redlock, Settings } from "@app/lib/red-lock"; export const PgSqlLock = { @@ -48,6 +50,13 @@ export const KeyStoreTtls = { AccessTokenStatusUpdateInSeconds: 120 }; +type TDeleteItems = { + pattern: string; + batchSize?: number; + delay?: number; + jitter?: number; +}; + type TWaitTillReady = { key: string; waitingCb?: () => void; @@ -75,6 +84,35 @@ export const keyStoreFactory = (redisUrl: string) => { const deleteItem = async (key: string) => redis.del(key); + const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => { + let cursor = "0"; + let totalDeleted = 0; + + do { + // Await in loop is needed so that Redis is not overwhelmed + // eslint-disable-next-line no-await-in-loop + const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads + cursor = nextCursor; + + for (let i = 0; i < keys.length; i += batchSize) { + const batch = keys.slice(i, i + batchSize); + const pipeline = redis.pipeline(); + for (const key of batch) { + pipeline.unlink(key); + } + // eslint-disable-next-line no-await-in-loop + await pipeline.exec(); + totalDeleted += batch.length; + console.log("BATCH DONE"); + + // eslint-disable-next-line no-await-in-loop + await delayMs(Math.max(0, applyJitter(delay, jitter))); + } + } while (cursor !== "0"); + + return totalDeleted; + }; + const incrementBy = async (key: string, value: number) => redis.incrby(key, value); const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds); @@ -94,7 +132,7 @@ export const keyStoreFactory = (redisUrl: string) => { // eslint-disable-next-line await new Promise((resolve) => { waitingCb?.(); - setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter))); + setTimeout(resolve, Math.max(0, applyJitter(delay, jitter))); }); attempts += 1; // eslint-disable-next-line @@ -108,6 +146,7 @@ export const keyStoreFactory = (redisUrl: string) => { setExpiry, setItemWithExpiry, deleteItem, + deleteItems, incrementBy, acquireLock(resources: string[], duration: number, settings?: Partial) { return redisLock.acquire(resources, duration, settings); diff --git a/backend/src/keystore/memory.ts b/backend/src/keystore/memory.ts index 10b28ffec..84cd06c03 100644 --- a/backend/src/keystore/memory.ts +++ b/backend/src/keystore/memory.ts @@ -1,3 +1,7 @@ +import RE2 from "re2"; + +import { applyJitter } from "@app/lib/dates"; +import { delay as delayMs } from "@app/lib/delay"; import { Lock } from "@app/lib/red-lock"; import { TKeyStoreFactory } from "./keystore"; @@ -19,6 +23,27 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => { delete store[key]; return 1; }, + deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => { + const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`); + let totalDeleted = 0; + const keys = Object.keys(store); + + for (let i = 0; i < keys.length; i += batchSize) { + const batch = keys.slice(i, i + batchSize); + + for (const key of batch) { + if (regex.test(key)) { + delete store[key]; + totalDeleted += 1; + } + } + + // eslint-disable-next-line no-await-in-loop + await delayMs(Math.max(0, applyJitter(delay, jitter))); + } + + return totalDeleted; + }, getItem: async (key) => { const value = store[key]; if (typeof value === "string") { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 2b2d86e33..6805575f2 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -18,6 +18,7 @@ export enum ApiDocsTags { KubernetesAuth = "Kubernetes Auth", JwtAuth = "JWT Auth", OidcAuth = "OIDC Auth", + LdapAuth = "LDAP Auth", Groups = "Groups", Organizations = "Organizations", Projects = "Projects", @@ -184,6 +185,49 @@ export const UNIVERSAL_AUTH = { } } as const; +export const LDAP_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login.", + username: "The username of the LDAP user to login.", + password: "The password of the LDAP user to login." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + url: "The URL of the LDAP server.", + allowedFields: + "The comma-separated array of key/value pairs of required fields that the LDAP entry must have in order to authenticate.", + searchBase: "The base DN to search for the LDAP user.", + searchFilter: "The filter to use to search for the LDAP user.", + bindDN: "The DN of the user to bind to the LDAP server.", + bindPass: "The password of the user to bind to the LDAP server.", + ldapCaCertificate: "The PEM-encoded CA certificate for the LDAP server.", + accessTokenTTL: "The lifetime for an access token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from." + }, + UPDATE: { + identityId: "The ID of the identity to update the configuration for.", + url: "The new URL of the LDAP server.", + allowedFields: "The comma-separated list of allowed fields to return from the LDAP user.", + searchBase: "The new base DN to search for the LDAP user.", + searchFilter: "The new filter to use to search for the LDAP user.", + bindDN: "The new DN of the user to bind to the LDAP server.", + bindPass: "The new password of the user to bind to the LDAP server.", + ldapCaCertificate: "The new PEM-encoded CA certificate for the LDAP server.", + 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." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the configuration for." + }, + REVOKE: { + identityId: "The ID of the identity to revoke the configuration for." + } +} as const; + export const AWS_AUTH = { LOGIN: { identityId: "The ID of the identity to login.", @@ -1619,7 +1663,8 @@ export const CERTIFICATES = { serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.", certificate: "The certificate body of the certificate.", certificateChain: "The certificate chain of the certificate.", - serialNumberRes: "The serial number of the certificate." + serialNumberRes: "The serial number of the certificate.", + privateKey: "The private key of the certificate." } }; @@ -1821,8 +1866,12 @@ export const KMS = { }; export const ProjectTemplates = { + LIST: { + type: "The type of project template to list." + }, CREATE: { name: "The name of the project template to be created. Must be slug-friendly.", + type: "The type of project template to be created.", description: "An optional description of the project template.", roles: "The roles to be created when the template is applied to a project.", environments: "The environments to be created when the template is applied to a project." diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 907884433..e38dbcfb5 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -146,6 +146,7 @@ const envSchema = z SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()), SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()), SECRET_SCANNING_ORG_WHITELIST: zpStr(z.string().optional()), + SECRET_SCANNING_GIT_APP_SLUG: zpStr(z.string().default("infisical-radar")), // LICENSE LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")), LICENSE_SERVER_KEY: zpStr(z.string().optional()), diff --git a/backend/src/lib/delay/index.ts b/backend/src/lib/delay/index.ts new file mode 100644 index 000000000..32cb8ebfc --- /dev/null +++ b/backend/src/lib/delay/index.ts @@ -0,0 +1,4 @@ +export const delay = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); diff --git a/backend/src/lib/logger/logger.ts b/backend/src/lib/logger/logger.ts index 170a0285f..afde8ef97 100644 --- a/backend/src/lib/logger/logger.ts +++ b/backend/src/lib/logger/logger.ts @@ -84,7 +84,9 @@ const redactedKeys = [ "secrets", "key", "password", - "config" + "config", + "bindPass", + "bindDN" ]; const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID"; diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index ae1a3e821..807d6c286 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -25,6 +25,7 @@ import { TQueueSecretSyncSyncSecretsByIdDTO, TQueueSendSecretSyncActionFailedNotificationsDTO } from "@app/services/secret-sync/secret-sync-types"; +import { CacheType } from "@app/services/super-admin/super-admin-types"; import { TWebhookPayloads } from "@app/services/webhook/webhook-types"; export enum QueueName { @@ -49,7 +50,8 @@ export enum QueueName { AccessTokenStatusUpdate = "access-token-status-update", ImportSecretsFromExternalSource = "import-secrets-from-external-source", AppConnectionSecretSync = "app-connection-secret-sync", - SecretRotationV2 = "secret-rotation-v2" + SecretRotationV2 = "secret-rotation-v2", + InvalidateCache = "invalidate-cache" } export enum QueueJobs { @@ -81,7 +83,8 @@ export enum QueueJobs { SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications", SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations", SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", - SecretRotationV2SendNotification = "secret-rotation-v2-send-notification" + SecretRotationV2SendNotification = "secret-rotation-v2-send-notification", + InvalidateCache = "invalidate-cache" } export type TQueueJobTypes = { @@ -234,6 +237,14 @@ export type TQueueJobTypes = { name: QueueJobs.SecretRotationV2SendNotification; payload: TSecretRotationSendNotificationJobPayload; }; + [QueueName.InvalidateCache]: { + name: QueueJobs.InvalidateCache; + payload: { + data: { + type: CacheType; + }; + }; + }; }; export type TQueueServiceFactory = ReturnType; diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 681442d1b..42bf37c71 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -100,3 +100,10 @@ export const publicSshCaLimit: RateLimitOptions = { max: 30, // conservative default keyGenerator: (req) => req.realIp }; + +export const invalidateCacheLimit: RateLimitOptions = { + timeWindow: 60 * 1000, + hook: "preValidation", + max: 1, + keyGenerator: (req) => req.realIp +}; diff --git a/backend/src/server/lib/caching.ts b/backend/src/server/lib/caching.ts new file mode 100644 index 000000000..513f2f635 --- /dev/null +++ b/backend/src/server/lib/caching.ts @@ -0,0 +1,8 @@ +import { FastifyReply } from "fastify"; + +export const addNoCacheHeaders = (reply: FastifyReply) => { + void reply.header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); + void reply.header("Pragma", "no-cache"); + void reply.header("Expires", "0"); + void reply.header("Surrogate-Control", "no-store"); +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index cbe008c9a..cab6fa6ff 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -126,6 +126,7 @@ import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal"; import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { certificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { certificateServiceFactory } from "@app/services/certificate/certificate-service"; import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; @@ -159,6 +160,8 @@ import { identityJwtAuthDALFactory } from "@app/services/identity-jwt-auth/ident import { identityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service"; import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal"; import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; +import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal"; +import { identityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service"; import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal"; import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; @@ -241,6 +244,7 @@ import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack- import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal"; import { slackServiceFactory } from "@app/services/slack/slack-service"; import { TSmtpService } from "@app/services/smtp/smtp-service"; +import { invalidateCacheQueueFactory } from "@app/services/super-admin/invalidate-cache-queue"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service"; import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal"; @@ -352,6 +356,7 @@ export const registerRoutes = async ( const identityOidcAuthDAL = identityOidcAuthDALFactory(db); const identityJwtAuthDAL = identityJwtAuthDALFactory(db); const identityAzureAuthDAL = identityAzureAuthDALFactory(db); + const identityLdapAuthDAL = identityLdapAuthDALFactory(db); const auditLogDAL = auditLogDALFactory(auditLogDb ?? db); const auditLogStreamDAL = auditLogStreamDALFactory(db); @@ -610,6 +615,11 @@ export const registerRoutes = async ( queueService }); + const invalidateCacheQueue = invalidateCacheQueueFactory({ + keyStore, + queueService + }); + const userService = userServiceFactory({ userDAL, userAliasDAL, @@ -721,7 +731,8 @@ export const registerRoutes = async ( keyStore, licenseService, kmsService, - microsoftTeamsService + microsoftTeamsService, + invalidateCacheQueue }); const orgAdminService = orgAdminServiceFactory({ @@ -812,6 +823,7 @@ export const registerRoutes = async ( const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); + const certificateSecretDAL = certificateSecretDALFactory(db); const pkiAlertDAL = pkiAlertDALFactory(db); const pkiCollectionDAL = pkiCollectionDALFactory(db); @@ -820,6 +832,7 @@ export const registerRoutes = async ( const certificateService = certificateServiceFactory({ certificateDAL, certificateBodyDAL, + certificateSecretDAL, certificateAuthorityDAL, certificateAuthorityCertDAL, certificateAuthorityCrlDAL, @@ -894,6 +907,7 @@ export const registerRoutes = async ( certificateAuthorityQueue, certificateDAL, certificateBodyDAL, + certificateSecretDAL, pkiCollectionDAL, pkiCollectionItemDAL, projectDAL, @@ -1437,6 +1451,16 @@ export const registerRoutes = async ( kmsService }); + const identityLdapAuthService = identityLdapAuthServiceFactory({ + identityLdapAuthDAL, + permissionService, + kmsService, + identityAccessTokenDAL, + identityOrgMembershipDAL, + licenseService, + identityDAL + }); + const gatewayService = gatewayServiceFactory({ permissionService, gatewayDAL, @@ -1697,6 +1721,7 @@ export const registerRoutes = async ( identityAzureAuth: identityAzureAuthService, identityOidcAuth: identityOidcAuthService, identityJwtAuth: identityJwtAuthService, + identityLdapAuth: identityLdapAuthService, accessApprovalPolicy: accessApprovalPolicyService, accessApprovalRequest: accessApprovalRequestService, secretApprovalPolicy: secretApprovalPolicyService, diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index a55aa2ba4..8610a611b 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -4,13 +4,14 @@ import { z } from "zod"; import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; -import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; -import { LoginMethod } from "@app/services/super-admin/super-admin-types"; +import { CacheType, LoginMethod } from "@app/services/super-admin/super-admin-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerAdminRouter = async (server: FastifyZodProvider) => { @@ -548,4 +549,69 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }; } }); + + server.route({ + method: "POST", + url: "/invalidate-cache", + config: { + rateLimit: invalidateCacheLimit + }, + schema: { + body: z.object({ + type: z.nativeEnum(CacheType) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + await server.services.superAdmin.invalidateCache(req.body.type); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.InvalidateCache, + distinctId: getTelemetryDistinctId(req), + properties: { + ...req.auditLogInfo + } + }); + + return { + message: "Cache invalidation job started" + }; + } + }); + + server.route({ + method: "GET", + url: "/invalidating-cache-status", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + invalidating: z.boolean() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async () => { + const invalidating = await server.services.superAdmin.checkIfInvalidatingCache(); + + return { + invalidating + }; + } + }); }; diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index ea33e948f..dad1d9a80 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ import { z } from "zod"; import { CertificatesSchema } from "@app/db/schemas"; @@ -5,6 +6,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { addNoCacheHeaders } from "@app/server/lib/caching"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -64,6 +66,111 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); + // TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best. + server.route({ + method: "GET", + url: "/:serialNumber/private-key", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Get certificate private key", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) + }), + response: { + 200: z.string().trim() + } + }, + handler: async (req, reply) => { + const { ca, cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CERT_PRIVATE_KEY, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + addNoCacheHeaders(reply); + + return certPrivateKey; + } + }); + + // TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best. + server.route({ + method: "GET", + url: "/:serialNumber/bundle", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Get certificate bundle including the certificate, chain, and private key.", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), + certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain), + privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey), + serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) + }) + } + }, + handler: async (req, reply) => { + const { certificate, certificateChain, serialNumber, cert, ca, privateKey } = + await server.services.certificate.getCertBundle({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CERT_BUNDLE, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + addNoCacheHeaders(reply); + + return { + certificate, + certificateChain, + serialNumber, + privateKey + }; + } + }); + server.route({ method: "POST", url: "/issue-certificate", @@ -411,7 +518,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), - certificateChain: z.string().trim().describe(CERTIFICATES.GET_CERT.certificateChain), + certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain), serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) }) } @@ -429,7 +536,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { ...req.auditLogInfo, projectId: ca.projectId, event: { - type: EventType.DELETE_CERT, + type: EventType.GET_CERT_BODY, metadata: { certId: cert.id, cn: cert.commonName, diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts new file mode 100644 index 000000000..3da8a425b --- /dev/null +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -0,0 +1,497 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +// All the any rules are disabled because passport typesense with fastify is really poor + +import { Authenticator } from "@fastify/passport"; +import fastifySession from "@fastify/session"; +import { FastifyRequest } from "fastify"; +import { IncomingMessage } from "http"; +import LdapStrategy from "passport-ldapauth"; +import { z } from "zod"; + +import { IdentityLdapAuthsSchema } from "@app/db/schemas/identity-ldap-auths"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { isValidLdapFilter } from "@app/ee/services/ldap-config/ldap-fns"; +import { ApiDocsTags, LDAP_AUTH } from "@app/lib/api-docs"; +import { getConfig } from "@app/lib/config/env"; +import { UnauthorizedError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; +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"; +import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { AllowedFieldsSchema } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; + +export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) => { + const appCfg = getConfig(); + const passport = new Authenticator({ key: "ldap-identity-auth", userProperty: "passportMachineIdentity" }); + await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY }); + await server.register(passport.initialize()); + await server.register(passport.secureSession()); + + const getLdapPassportOpts = (req: FastifyRequest, done: any) => { + const { identityId } = req.body as { + identityId: string; + }; + + process.nextTick(async () => { + try { + const { ldapConfig, opts } = await server.services.identityLdapAuth.getLdapConfig(identityId); + req.ldapConfig = { + ...ldapConfig, + isActive: true, + groupSearchBase: "", + uniqueUserAttribute: "", + groupSearchFilter: "" + }; + + done(null, opts); + } catch (err) { + logger.error(err, "Error in LDAP verification callback"); + done(err); + } + }); + }; + + passport.use( + new LdapStrategy( + getLdapPassportOpts as any, + // eslint-disable-next-line + async (req: IncomingMessage, user, cb) => { + try { + const requestBody = (req as unknown as FastifyRequest).body as { + username: string; + password: string; + identityId: string; + }; + + if (!requestBody.username || !requestBody.password) { + return cb(new UnauthorizedError({ message: "Invalid request. Missing username or password." }), false); + } + + if (!requestBody.identityId) { + return cb(new UnauthorizedError({ message: "Invalid request. Missing identity ID." }), false); + } + + const { ldapConfig } = req as unknown as FastifyRequest; + + if (ldapConfig.allowedFields) { + for (const field of ldapConfig.allowedFields) { + if (!user[field.key]) { + return cb( + new UnauthorizedError({ message: `Invalid request. Missing field ${field.key} on user.` }), + false + ); + } + + const value = field.value.split(","); + + if (!value.includes(user[field.key])) { + return cb( + new UnauthorizedError({ + message: `Invalid request. User field '${field.key}' does not match required fields.` + }), + false + ); + } + } + } + + return cb(null, { identityId: requestBody.identityId, user }); + } catch (error) { + logger.error(error, "Error in LDAP verification callback"); + return cb(error, false); + } + } + ) + ); + + server.route({ + method: "POST", + url: "/ldap-auth/login", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.LdapAuth], + description: "Login with LDAP Auth", + body: z.object({ + identityId: z.string().trim().describe(LDAP_AUTH.LOGIN.identityId), + username: z.string().describe(LDAP_AUTH.LOGIN.username), + password: z.string().describe(LDAP_AUTH.LOGIN.password) + }), + response: { + 200: z.object({ + accessToken: z.string(), + expiresIn: z.coerce.number(), + accessTokenMaxTTL: z.coerce.number(), + tokenType: z.literal("Bearer") + }) + } + }, + preValidation: passport.authenticate("ldapauth", { + failWithError: true, + session: false + }) as any, + + errorHandler: (error) => { + if (error.name === "AuthenticationError") { + throw new UnauthorizedError({ message: "Invalid credentials" }); + } + + throw error; + }, + + handler: async (req) => { + if (!req.passportMachineIdentity?.identityId) { + throw new UnauthorizedError({ message: "Invalid request. Missing identity ID or LDAP entry details." }); + } + + const { identityId, user } = req.passportMachineIdentity; + + const { accessToken, identityLdapAuth, identityMembershipOrg } = await server.services.identityLdapAuth.login({ + identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg?.orgId, + event: { + type: EventType.LOGIN_IDENTITY_LDAP_AUTH, + metadata: { + identityId, + ldapEmail: user.mail, + ldapUsername: user.uid + } + } + }); + + return { + accessToken, + tokenType: "Bearer" as const, + expiresIn: identityLdapAuth.accessTokenTTL, + accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL + }; + } + }); + + server.route({ + method: "POST", + url: "/ldap-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.LdapAuth], + description: "Attach LDAP Auth configuration onto identity", + security: [ + { + bearerAuth: [] + } + ], + 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." + ), + response: { + 200: z.object({ + identityLdapAuth: IdentityLdapAuthsSchema.omit({ + encryptedBindDN: true, + encryptedBindPass: true, + encryptedLdapCaCertificate: true + }) + }) + } + }, + handler: async (req) => { + const identityLdapAuth = await server.services.identityLdapAuth.attachLdapAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.ADD_IDENTITY_LDAP_AUTH, + metadata: { + identityId: req.params.identityId, + url: identityLdapAuth.url, + accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, + accessTokenTTL: identityLdapAuth.accessTokenTTL, + accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, + allowedFields: req.body.allowedFields + } + } + }); + + return { identityLdapAuth }; + } + }); + + server.route({ + method: "PATCH", + url: "/ldap-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.LdapAuth], + description: "Update LDAP Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(LDAP_AUTH.UPDATE.identityId) + }), + body: z + .object({ + url: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.url), + 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), + searchFilter: z + .string() + .trim() + .min(1) + .optional() + .refine((v) => v === undefined || isValidLdapFilter(v), "Invalid LDAP search filter") + .describe(LDAP_AUTH.UPDATE.searchFilter), + allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.UPDATE.allowedFields), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(LDAP_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(LDAP_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(LDAP_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .min(0) + .optional() + .describe(LDAP_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), + response: { + 200: z.object({ + identityLdapAuth: IdentityLdapAuthsSchema.omit({ + encryptedBindDN: true, + encryptedBindPass: true, + encryptedLdapCaCertificate: true + }) + }) + } + }, + handler: async (req) => { + const identityLdapAuth = await server.services.identityLdapAuth.updateLdapAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.UPDATE_IDENTITY_LDAP_AUTH, + metadata: { + identityId: req.params.identityId, + url: identityLdapAuth.url, + accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, + accessTokenTTL: identityLdapAuth.accessTokenTTL, + accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, + accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + allowedFields: req.body.allowedFields + } + } + }); + + return { identityLdapAuth }; + } + }); + + server.route({ + method: "GET", + url: "/ldap-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.LdapAuth], + description: "Retrieve LDAP Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(LDAP_AUTH.RETRIEVE.identityId) + }), + response: { + 200: z.object({ + identityLdapAuth: IdentityLdapAuthsSchema.omit({ + encryptedBindDN: true, + encryptedBindPass: true, + encryptedLdapCaCertificate: true + }).extend({ + bindDN: z.string(), + bindPass: z.string(), + ldapCaCertificate: z.string().optional() + }) + }) + } + }, + handler: async (req) => { + const identityLdapAuth = await server.services.identityLdapAuth.getLdapAuth({ + identityId: req.params.identityId, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.GET_IDENTITY_LDAP_AUTH, + metadata: { + identityId: identityLdapAuth.identityId + } + } + }); + + return { identityLdapAuth }; + } + }); + + server.route({ + method: "DELETE", + url: "/ldap-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.LdapAuth], + description: "Delete LDAP Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(LDAP_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityLdapAuth: IdentityLdapAuthsSchema.omit({ + encryptedBindDN: true, + encryptedBindPass: true, + encryptedLdapCaCertificate: true + }) + }) + } + }, + handler: async (req) => { + const identityLdapAuth = await server.services.identityLdapAuth.revokeIdentityLdapAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.REVOKE_IDENTITY_LDAP_AUTH, + metadata: { + identityId: identityLdapAuth.identityId + } + } + }); + + return { identityLdapAuth }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index a50299555..b1a49f815 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -19,6 +19,7 @@ import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; +import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; @@ -63,6 +64,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await authRouter.register(registerIdentityAzureAuthRouter); await authRouter.register(registerIdentityOidcAuthRouter); await authRouter.register(registerIdentityJwtAuthRouter); + await authRouter.register(registerIdentityLdapAuthRouter); }, { prefix: "/auth" } ); diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index a223004a9..498fb8e8b 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -170,7 +170,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .optional() .default(InfisicalProjectTemplate.Default) .describe(PROJECTS.CREATE.template), - type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager) + type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager), + shouldCreateDefaultEnvs: z.boolean().optional().default(true) }), response: { 200: z.object({ @@ -190,7 +191,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { slug: req.body.slug, kmsKeyId: req.body.kmsKeyId, template: req.body.template, - type: req.body.type + type: req.body.type, + createDefaultEnvs: req.body.shouldCreateDefaultEnvs }); await server.services.telemetry.sendPostHogEvents({ @@ -272,7 +274,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, schema: { params: z.object({ - slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.") + slug: slugSchema({ max: 36 }).describe("The slug of the project to get.") }), response: { 200: projectWithEnv diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 499a25741..e1d7ce5cb 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -6,7 +6,11 @@ import { z } from "zod"; import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionActions, + ProjectPermissionCertificateActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -21,6 +25,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; +import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import { CertExtendedKeyUsage, CertExtendedKeyUsageOIDToName, @@ -75,6 +80,7 @@ type TCertificateAuthorityServiceFactoryDep = { certificateTemplateDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateDAL: Pick; + certificateSecretDAL: Pick; certificateBodyDAL: Pick; pkiCollectionDAL: Pick; pkiCollectionItemDAL: Pick; @@ -96,6 +102,7 @@ export const certificateAuthorityServiceFactory = ({ certificateTemplateDAL, certificateDAL, certificateBodyDAL, + certificateSecretDAL, pkiCollectionDAL, pkiCollectionItemDAL, projectDAL, @@ -1157,7 +1164,10 @@ export const certificateAuthorityServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Create, + ProjectPermissionSub.Certificates + ); if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); @@ -1373,6 +1383,23 @@ export const certificateAuthorityServiceFactory = ({ const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }); + const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ + plainText: Buffer.from(skLeaf) + }); + + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caCertId: caCert.id, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim(); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); await certificateDAL.transaction(async (tx) => { const cert = await certificateDAL.create( @@ -1396,7 +1423,16 @@ export const certificateAuthorityServiceFactory = ({ await certificateBodyDAL.create( { certId: cert.id, - encryptedCertificate + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + await certificateSecretDAL.create( + { + certId: cert.id, + encryptedPrivateKey }, tx ); @@ -1414,17 +1450,9 @@ export const certificateAuthorityServiceFactory = ({ return cert; }); - const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ - caCertId: caCert.id, - certificateAuthorityDAL, - certificateAuthorityCertDAL, - projectDAL, - kmsService - }); - return { certificate: leafCert.toString("pem"), - certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), + certificateChain: certificateChainPem, issuingCaCertificate, privateKey: skLeaf, serialNumber, @@ -1487,7 +1515,7 @@ export const certificateAuthorityServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionCertificateActions.Create, ProjectPermissionSub.Certificates ); } diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index 45ad5963c..961fb27ff 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -1,6 +1,11 @@ +import crypto from "node:crypto"; + import * as x509 from "@peculiar/x509"; -import { CrlReason } from "./certificate-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; + +import { getProjectKmsCertificateKeyId } from "../project/project-fns"; +import { CrlReason, TBuildCertificateChainDTO, TGetCertificateCredentialsDTO } from "./certificate-types"; export const revocationReasonToCrlCode = (crlReason: CrlReason) => { switch (crlReason) { @@ -46,3 +51,73 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) .map((cert) => cert.toString("pem")) .join("\n") .trim(); + +/** + * Return the public and private key of certificate + * Note: credentials are returned as PEM strings + */ +export const getCertificateCredentials = async ({ + certId, + projectId, + certificateSecretDAL, + projectDAL, + kmsService +}: TGetCertificateCredentialsDTO) => { + const certificateSecret = await certificateSecretDAL.findOne({ certId }); + if (!certificateSecret) + throw new NotFoundError({ message: `Certificate secret for certificate with ID '${certId}' not found` }); + + const keyId = await getProjectKmsCertificateKeyId({ + projectId, + projectDAL, + kmsService + }); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: keyId + }); + const decryptedPrivateKey = await kmsDecryptor({ + cipherTextBlob: certificateSecret.encryptedPrivateKey + }); + + try { + const skObj = crypto.createPrivateKey({ key: decryptedPrivateKey, format: "pem", type: "pkcs8" }); + const certPrivateKey = skObj.export({ format: "pem", type: "pkcs8" }).toString(); + + const pkObj = crypto.createPublicKey(skObj); + const certPublicKey = pkObj.export({ format: "pem", type: "spki" }).toString(); + + return { + certificateSecret, + certPrivateKey, + certPublicKey + }; + } catch (error) { + throw new BadRequestError({ message: `Failed to process private key for certificate with ID '${certId}'` }); + } +}; + +// If the certificate was generated after ~05/01/25 it will have a encryptedCertificateChain attached to it's body +// Otherwise we'll fallback to manually building the chain +export const buildCertificateChain = async ({ + caCert, + caCertChain, + encryptedCertificateChain, + kmsService, + kmsId +}: TBuildCertificateChainDTO) => { + if (!encryptedCertificateChain && (!caCert || !caCertChain)) { + return null; + } + + let certificateChain = `${caCert}\n${caCertChain}`.trim(); + + if (encryptedCertificateChain) { + const kmsDecryptor = await kmsService.decryptWithKmsKey({ kmsId }); + const decryptedCertChain = await kmsDecryptor({ + cipherTextBlob: encryptedCertificateChain + }); + certificateChain = decryptedCertChain.toString(); + } + + return certificateChain; +}; diff --git a/backend/src/services/certificate/certificate-secret-dal.ts b/backend/src/services/certificate/certificate-secret-dal.ts new file mode 100644 index 000000000..c1493eceb --- /dev/null +++ b/backend/src/services/certificate/certificate-secret-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TCertificateSecretDALFactory = ReturnType; + +export const certificateSecretDALFactory = (db: TDbClient) => { + const certSecretOrm = ormify(db, TableName.CertificateSecret); + return certSecretOrm; +}; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 0ca0d64c6..73a8caed7 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -4,7 +4,10 @@ import * as x509 from "@peculiar/x509"; import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; @@ -15,11 +18,21 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; -import { revocationReasonToCrlCode } from "./certificate-fns"; -import { CertStatus, TDeleteCertDTO, TGetCertBodyDTO, TGetCertDTO, TRevokeCertDTO } from "./certificate-types"; +import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns"; +import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; +import { + CertStatus, + TDeleteCertDTO, + TGetCertBodyDTO, + TGetCertBundleDTO, + TGetCertDTO, + TGetCertPrivateKeyDTO, + TRevokeCertDTO +} from "./certificate-types"; type TCertificateServiceFactoryDep = { certificateDAL: Pick; + certificateSecretDAL: Pick; certificateBodyDAL: Pick; certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; @@ -34,6 +47,7 @@ export type TCertificateServiceFactory = ReturnType { + const cert = await certificateDAL.findOne({ serialNumber }); + const ca = await certificateAuthorityDAL.findById(cert.caId); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates + ); + + const { certPrivateKey } = await getCertificateCredentials({ + certId: cert.id, + projectId: ca.projectId, + certificateSecretDAL, + projectDAL, + kmsService + }); + + return { + ca, + cert, + certPrivateKey + }; + }; + /** * Delete certificate with serial number [serialNumber] */ @@ -83,7 +142,10 @@ export const certificateServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Delete, + ProjectPermissionSub.Certificates + ); const deletedCert = await certificateDAL.deleteById(cert.id); @@ -118,7 +180,10 @@ export const certificateServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Delete, + ProjectPermissionSub.Certificates + ); if (cert.status === CertStatus.REVOKED) throw new Error("Certificate already revoked"); @@ -165,7 +230,10 @@ export const certificateServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); @@ -192,19 +260,107 @@ export const certificateServiceFactory = ({ kmsService }); + const certificateChain = await buildCertificateChain({ + caCert, + caCertChain, + kmsId: certificateManagerKeyId, + kmsService, + encryptedCertificateChain: certBody.encryptedCertificateChain || undefined + }); + return { certificate: certObj.toString("pem"), - certificateChain: `${caCert}\n${caCertChain}`.trim(), + certificateChain, serialNumber: certObj.serialNumber, cert, ca }; }; + /** + * Return certificate body and certificate chain for certificate with + * serial number [serialNumber] + */ + const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => { + const cert = await certificateDAL.findOne({ serialNumber }); + const ca = await certificateAuthorityDAL.findById(cert.caId); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates + ); + + const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); + + const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKeyId + }); + const decryptedCert = await kmsDecryptor({ + cipherTextBlob: certBody.encryptedCertificate + }); + + const certObj = new x509.X509Certificate(decryptedCert); + const certificate = certObj.toString("pem"); + + const { caCert, caCertChain } = await getCaCertChain({ + caCertId: cert.caCertId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + const certificateChain = await buildCertificateChain({ + caCert, + caCertChain, + kmsId: certificateManagerKeyId, + kmsService, + encryptedCertificateChain: certBody.encryptedCertificateChain || undefined + }); + + const { certPrivateKey } = await getCertificateCredentials({ + certId: cert.id, + projectId: ca.projectId, + certificateSecretDAL, + projectDAL, + kmsService + }); + + return { + certificate, + certificateChain, + privateKey: certPrivateKey, + serialNumber, + cert, + ca + }; + }; + return { getCert, + getCertPrivateKey, deleteCert, revokeCert, - getCertBody + getCertBody, + getCertBundle }; }; diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index ef63f142d..ae04eae6b 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -2,6 +2,10 @@ import * as x509 from "@peculiar/x509"; import { TProjectPermission } from "@app/lib/types"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { TProjectDALFactory } from "../project/project-dal"; +import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; + export enum CertStatus { ACTIVE = "active", REVOKED = "revoked" @@ -73,3 +77,27 @@ export type TRevokeCertDTO = { export type TGetCertBodyDTO = { serialNumber: string; } & Omit; + +export type TGetCertPrivateKeyDTO = { + serialNumber: string; +} & Omit; + +export type TGetCertBundleDTO = { + serialNumber: string; +} & Omit; + +export type TGetCertificateCredentialsDTO = { + certId: string; + projectId: string; + certificateSecretDAL: Pick; + projectDAL: Pick; + kmsService: Pick; +}; + +export type TBuildCertificateChainDTO = { + caCert?: string; + caCertChain?: string; + encryptedCertificateChain?: Buffer; + kmsService: Pick; + kmsId: string; +}; diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index 57517c706..a2a067cad 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -30,6 +30,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { .leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`) .leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`) .leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`) + .leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`) .leftJoin( TableName.IdentityKubernetesAuth, `${TableName.Identity}.id`, @@ -48,6 +49,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"), + db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityLdapAuth).as("accessTokenTrustedIpsLdap"), db.ref("name").withSchema(TableName.Identity) ) .first(); @@ -63,7 +65,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s, trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc, trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken, - trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt + trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt, + trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap }; } catch (error) { throw new DatabaseError({ error, name: "IdAccessTokenFindOne" }); diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index a51d80e41..cd79981fe 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -186,7 +186,8 @@ export const identityAccessTokenServiceFactory = ({ [IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth, [IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth, [IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth, - [IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth + [IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth, + [IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth }; const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod]; diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-dal.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-dal.ts new file mode 100644 index 000000000..0d998dbe9 --- /dev/null +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityLdapAuthDALFactory = ReturnType; + +export const identityLdapAuthDALFactory = (db: TDbClient) => { + const ldapAuthOrm = ormify(db, TableName.IdentityLdapAuth); + + return ldapAuthOrm; +}; 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 new file mode 100644 index 000000000..7462c9228 --- /dev/null +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -0,0 +1,543 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ForbiddenError } from "@casl/ability"; +import jwt from "jsonwebtoken"; + +import { IdentityAuthMethod } from "@app/db/schemas"; +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 { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; +import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; + +import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; +import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; +import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; +import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TKmsServiceFactory } from "../kms/kms-service"; +import { KmsDataKey } from "../kms/kms-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; +import { TIdentityLdapAuthDALFactory } from "./identity-ldap-auth-dal"; +import { + AllowedFieldsSchema, + TAttachLdapAuthDTO, + TGetLdapAuthDTO, + TLoginLdapAuthDTO, + TRevokeLdapAuthDTO, + TUpdateLdapAuthDTO +} from "./identity-ldap-auth-types"; + +type TIdentityLdapAuthServiceFactoryDep = { + identityAccessTokenDAL: Pick; + identityLdapAuthDAL: Pick< + TIdentityLdapAuthDALFactory, + "findOne" | "transaction" | "create" | "updateById" | "delete" + >; + identityOrgMembershipDAL: Pick; + licenseService: Pick; + permissionService: Pick; + kmsService: TKmsServiceFactory; + identityDAL: TIdentityDALFactory; +}; + +export type TIdentityLdapAuthServiceFactory = ReturnType; + +export const identityLdapAuthServiceFactory = ({ + identityAccessTokenDAL, + identityDAL, + identityLdapAuthDAL, + identityOrgMembershipDAL, + licenseService, + permissionService, + kmsService +}: TIdentityLdapAuthServiceFactoryDep) => { + const getLdapConfig = async (identityId: string) => { + const identity = await identityDAL.findOne({ id: identityId }); + if (!identity) throw new NotFoundError({ message: `Identity with ID '${identityId}' not found` }); + + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: identity.id }); + if (!identityOrgMembership) throw new NotFoundError({ message: `Identity with ID '${identityId}' not found` }); + + const ldapAuth = await identityLdapAuthDAL.findOne({ identityId: identity.id }); + if (!ldapAuth) throw new NotFoundError({ message: `LDAP auth with ID '${identityId}' not found` }); + + const parsedAllowedFields = ldapAuth.allowedFields + ? AllowedFieldsSchema.array().parse(ldapAuth.allowedFields) + : undefined; + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityOrgMembership.orgId + }); + + const bindDN = decryptor({ cipherTextBlob: ldapAuth.encryptedBindDN }).toString(); + const bindPass = decryptor({ cipherTextBlob: ldapAuth.encryptedBindPass }).toString(); + const ldapCaCertificate = ldapAuth.encryptedLdapCaCertificate + ? decryptor({ cipherTextBlob: ldapAuth.encryptedLdapCaCertificate }).toString() + : undefined; + + const ldapConfig = { + id: ldapAuth.id, + organization: identityOrgMembership.orgId, + url: ldapAuth.url, + bindDN, + bindPass, + searchBase: ldapAuth.searchBase, + searchFilter: ldapAuth.searchFilter, + caCert: ldapCaCertificate || "", + allowedFields: parsedAllowedFields + }; + + const opts = { + server: { + url: ldapAuth.url, + bindDN, + bindCredentials: bindPass, + searchBase: ldapAuth.searchBase, + searchFilter: ldapAuth.searchFilter, + ...(ldapCaCertificate + ? { + tlsOptions: { + ca: [ldapCaCertificate] + } + } + : {}) + }, + passReqToCallback: true + }; + + return { opts, ldapConfig }; + }; + + const login = async ({ identityId }: TLoginLdapAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + + if (!identityMembershipOrg) { + throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + } + + const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); + + if (!identityLdapAuth) { + throw new NotFoundError({ message: `Failed to find LDAP auth for identity with ID ${identityId}` }); + } + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + if (!plan.ldap) { + throw new BadRequestError({ + message: + "Failed to login to identity due to plan restriction. Upgrade plan to login to use LDAP authentication." + }); + } + + const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityLdapAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityLdapAuth.accessTokenTTL, + accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.LDAP_AUTH + }, + tx + ); + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityLdapAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } + ); + + return { accessToken, identityLdapAuth, identityAccessToken, identityMembershipOrg }; + }; + + const attachLdapAuth = async ({ + identityId, + url, + searchBase, + searchFilter, + bindDN, + bindPass, + ldapCaCertificate, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId, + isActorSuperAdmin, + allowedFields + }: TAttachLdapAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { + throw new BadRequestError({ + message: "Failed to add LDAP Auth to already configured identity" + }); + } + + if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + + if (!plan.ldap) { + throw new BadRequestError({ + message: "Failed to add LDAP Auth to identity due to plan restriction. Upgrade plan to add LDAP Auth." + }); + } + + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields); + + const identityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => { + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId + }); + + const { cipherTextBlob: encryptedBindPass } = encryptor({ + plainText: Buffer.from(bindPass) + }); + + let encryptedLdapCaCertificate: Buffer | undefined; + if (ldapCaCertificate) { + const { cipherTextBlob: encryptedCertificate } = encryptor({ + plainText: Buffer.from(ldapCaCertificate) + }); + + encryptedLdapCaCertificate = encryptedCertificate; + } + + const { cipherTextBlob: encryptedBindDN } = encryptor({ + plainText: Buffer.from(bindDN) + }); + + const isConnected = await testLDAPConfig({ + bindDN, + bindPass, + caCert: ldapCaCertificate || "", + url + }); + + if (!isConnected) { + throw new BadRequestError({ + message: + "Failed to connect to LDAP server. Please ensure that the LDAP server is running and your credentials are correct." + }); + } + + const doc = await identityLdapAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + encryptedBindDN, + encryptedBindPass, + searchBase, + searchFilter, + url, + encryptedLdapCaCertificate, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), + allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined + }, + tx + ); + return doc; + }); + return { ...identityLdapAuth, orgId: identityMembershipOrg.orgId }; + }; + + const updateLdapAuth = async ({ + identityId, + url, + searchBase, + searchFilter, + bindDN, + bindPass, + ldapCaCertificate, + allowedFields, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateLdapAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { + throw new NotFoundError({ + message: "The identity does not have LDAP Auth attached" + }); + } + + const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityLdapAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityLdapAuth.accessTokenTTL) > (accessTokenMaxTTL || identityLdapAuth.accessTokenMaxTTL) + ) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + + if (!plan.ldap) { + throw new BadRequestError({ + message: "Failed to update LDAP Auth due to plan restriction. Upgrade plan to update LDAP Auth." + }); + } + + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields); + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId + }); + + let encryptedBindPass: Buffer | undefined; + if (bindPass) { + const { cipherTextBlob: bindPassCiphertext } = encryptor({ + plainText: Buffer.from(bindPass) + }); + + encryptedBindPass = bindPassCiphertext; + } + + let encryptedLdapCaCertificate: Buffer | undefined; + if (ldapCaCertificate) { + const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({ + plainText: Buffer.from(ldapCaCertificate) + }); + + encryptedLdapCaCertificate = ldapCaCertificateCiphertext; + } + + let encryptedBindDN: Buffer | undefined; + if (bindDN) { + const { cipherTextBlob: bindDNCiphertext } = encryptor({ + plainText: Buffer.from(bindDN) + }); + + encryptedBindDN = bindDNCiphertext; + } + + 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 + }); + + if (!isConnected) { + throw new BadRequestError({ + message: + "Failed to connect to LDAP server. Please ensure that the LDAP server is running and your credentials are correct." + }); + } + + const updatedLdapAuth = await identityLdapAuthDAL.updateById(identityLdapAuth.id, { + url, + searchBase, + searchFilter, + encryptedBindDN, + encryptedBindPass, + encryptedLdapCaCertificate, + allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }); + + return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId }; + }; + + const getLdapAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetLdapAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have LDAP Auth attached" + }); + } + + const ldapIdentityAuth = await identityLdapAuthDAL.findOne({ identityId }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId + }); + + const bindDN = decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedBindDN }).toString(); + const bindPass = decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedBindPass }).toString(); + const ldapCaCertificate = ldapIdentityAuth.encryptedLdapCaCertificate + ? decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedLdapCaCertificate }).toString() + : undefined; + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + return { ...ldapIdentityAuth, orgId: identityMembershipOrg.orgId, bindDN, bindPass, ldapCaCertificate }; + }; + + const revokeIdentityLdapAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeLdapAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have LDAP Auth attached" + }); + } + const { permission, membership } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke LDAP auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const revokedIdentityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => { + const [deletedLdapAuth] = await identityLdapAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.LDAP_AUTH }, tx); + + return { ...deletedLdapAuth, orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityLdapAuth; + }; + + return { + attachLdapAuth, + getLdapConfig, + updateLdapAuth, + login, + revokeIdentityLdapAuth, + getLdapAuth + }; +}; 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 new file mode 100644 index 000000000..0e6feb5fb --- /dev/null +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; + +import { TProjectPermission } from "@app/lib/types"; + +export const AllowedFieldsSchema = z.object({ + key: z.string().trim(), + value: z + .string() + .trim() + .transform((val) => val.replace(/\s/g, "")) +}); + +export type TAllowedFields = z.infer; + +export type TAttachLdapAuthDTO = { + identityId: string; + url: string; + searchBase: string; + searchFilter: string; + bindDN: string; + bindPass: string; + ldapCaCertificate?: string; + allowedFields?: TAllowedFields[]; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; +} & Omit; + +export type TUpdateLdapAuthDTO = { + identityId: string; + url?: string; + searchBase?: string; + searchFilter?: string; + bindDN?: string; + bindPass?: string; + allowedFields?: TAllowedFields[]; + ldapCaCertificate?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { ipAddress: string }[]; +} & Omit; + +export type TGetLdapAuthDTO = { + identityId: string; +} & Omit; + +export type TLoginLdapAuthDTO = { + identityId: string; +}; + +export type TRevokeLdapAuthDTO = { + identityId: string; +} & Omit; diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts index 2d77e6544..6c77618e4 100644 --- a/backend/src/services/identity/identity-fns.ts +++ b/backend/src/services/identity/identity-fns.ts @@ -8,7 +8,8 @@ export const buildAuthMethods = ({ oidcId, azureId, tokenId, - jwtId + jwtId, + ldapId }: { uaId?: string; gcpId?: string; @@ -18,6 +19,7 @@ export const buildAuthMethods = ({ azureId?: string; tokenId?: string; jwtId?: string; + ldapId?: string; }) => { return [ ...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null], @@ -27,6 +29,7 @@ export const buildAuthMethods = ({ ...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null], ...[azureId ? IdentityAuthMethod.AZURE_AUTH : null], ...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null], - ...[jwtId ? IdentityAuthMethod.JWT_AUTH : null] + ...[jwtId ? IdentityAuthMethod.JWT_AUTH : null], + ...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null] ].filter((authMethod) => authMethod) as IdentityAuthMethod[]; }; diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index dbae59bbe..8b5032945 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -14,6 +14,7 @@ import { TIdentityUniversalAuths, TOrgRoles } from "@app/db/schemas"; +import { TIdentityLdapAuths } from "@app/db/schemas/identity-ldap-auths"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db"; @@ -81,6 +82,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { `${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityJwtAuth}.identityId` ) + .leftJoin( + TableName.IdentityLdapAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityLdapAuth}.identityId` + ) .select( selectAllTableCols(TableName.IdentityOrgMembership), @@ -93,7 +99,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), - + db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth), db.ref("name").withSchema(TableName.Identity) ); @@ -200,6 +206,12 @@ export const identityOrgDALFactory = (db: TDbClient) => { "paginatedIdentity.identityId", `${TableName.IdentityJwtAuth}.identityId` ) + .leftJoin( + TableName.IdentityLdapAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityLdapAuth}.identityId` + ) + .select( db.ref("id").withSchema("paginatedIdentity"), db.ref("role").withSchema("paginatedIdentity"), @@ -217,7 +229,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), - db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth) + db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), + db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth) ) // cr stands for custom role .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) @@ -259,6 +272,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { oidcId, azureId, tokenId, + ldapId, createdAt, updatedAt }) => ({ @@ -290,7 +304,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { oidcId, azureId, tokenId, - jwtId + jwtId, + ldapId }) } }), @@ -406,6 +421,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { `${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityJwtAuth}.identityId` ) + .leftJoin( + TableName.IdentityLdapAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityLdapAuth}.identityId` + ) .select( db.ref("id").withSchema(TableName.IdentityOrgMembership), db.ref("total_count").withSchema("searchedIdentities"), @@ -424,7 +444,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), - db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth) + db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), + db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth) ) // cr stands for custom role .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) @@ -467,6 +488,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { oidcId, azureId, tokenId, + ldapId, createdAt, updatedAt }) => ({ @@ -498,7 +520,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { oidcId, azureId, tokenId, - jwtId + jwtId, + ldapId }) } }), diff --git a/backend/src/services/project-role/project-role-fns.ts b/backend/src/services/project-role/project-role-fns.ts index c465715a7..4dfcf960b 100644 --- a/backend/src/services/project-role/project-role-fns.ts +++ b/backend/src/services/project-role/project-role-fns.ts @@ -1,15 +1,20 @@ -import { ProjectMembershipRole } from "@app/db/schemas"; +import { v4 as uuidv4 } from "uuid"; + +import { ProjectMembershipRole, ProjectType } from "@app/db/schemas"; import { + cryptographicOperatorPermissions, projectAdminPermissions, projectMemberPermissions, projectNoAccessPermissions, - projectViewerPermission -} from "@app/ee/services/permission/project-permission"; + projectViewerPermission, + sshHostBootstrapPermissions +} from "@app/ee/services/permission/default-roles"; +import { TGetPredefinedRolesDTO } from "@app/services/project-role/project-role-types"; -export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => { +export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetPredefinedRolesDTO) => { return [ { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid + id: uuidv4(), projectId, name: "Admin", slug: ProjectMembershipRole.Admin, @@ -19,7 +24,7 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember updatedAt: new Date() }, { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response + id: uuidv4(), projectId, name: "Developer", slug: ProjectMembershipRole.Member, @@ -29,7 +34,29 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember updatedAt: new Date() }, { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response + id: uuidv4(), + projectId, + name: "SSH Host Bootstrapper", + slug: ProjectMembershipRole.SshHostBootstrapper, + permissions: sshHostBootstrapPermissions, + description: "Create and issue SSH Hosts in a project", + createdAt: new Date(), + updatedAt: new Date(), + type: ProjectType.SSH + }, + { + id: uuidv4(), + projectId, + name: "Cryptographic Operator", + slug: ProjectMembershipRole.KmsCryptographicOperator, + permissions: cryptographicOperatorPermissions, + description: "Perform cryptographic operations, such as encryption and signing, in a project", + createdAt: new Date(), + updatedAt: new Date(), + type: ProjectType.KMS + }, + { + id: uuidv4(), projectId, name: "Viewer", slug: ProjectMembershipRole.Viewer, @@ -39,7 +66,7 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember updatedAt: new Date() }, { - id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response + id: uuidv4(), projectId, name: "No Access", slug: ProjectMembershipRole.NoAccess, @@ -48,5 +75,5 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember createdAt: new Date(), updatedAt: new Date() } - ].filter(({ slug }) => !roleFilter || roleFilter.includes(slug)); + ].filter(({ slug, type }) => (type ? type === projectType : true) && (!roleFilter || roleFilter === slug)); }; diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 211dcff4f..babcf7d9c 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { requestContext } from "@fastify/request-context"; -import { ActionProjectType, ProjectMembershipRole, TableName } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, ProjectType, TableName, TProjects } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -34,7 +34,7 @@ type TProjectRoleServiceFactoryDep = { projectRoleDAL: TProjectRoleDALFactory; identityDAL: Pick; userDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; permissionService: Pick; identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory; projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory; @@ -98,30 +98,37 @@ export const projectRoleServiceFactory = ({ roleSlug, filter }: TGetRoleDetailsDTO) => { - let projectId = ""; + let project: TProjects; if (filter.type === ProjectRoleServiceIdentifierType.SLUG) { - const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: "Project not found" }); - projectId = project.id; + project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); } else { - projectId = filter.projectId; + project = await projectDAL.findProjectById(filter.projectId); } + if (!project) throw new NotFoundError({ message: "Project not found" }); + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId, + projectId: project.id, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) { - const predefinedRole = getPredefinedRoles(projectId, roleSlug as ProjectMembershipRole)[0]; + const [predefinedRole] = getPredefinedRoles({ + projectId: project.id, + projectType: project.type as ProjectType, + roleFilter: roleSlug as ProjectMembershipRole + }); + + if (!predefinedRole) throw new NotFoundError({ message: `Default role with slug '${roleSlug}' not found` }); + return { ...predefinedRole, permissions: UnpackedPermissionSchema.array().parse(predefinedRole.permissions) }; } - const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId }); + const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId: project.id }); if (!customRole) throw new NotFoundError({ message: `Project role with slug '${roleSlug}' not found` }); return { ...customRole, permissions: unpackPermissions(customRole.permissions) }; }; @@ -194,29 +201,32 @@ export const projectRoleServiceFactory = ({ }; const listRoles = async ({ actorOrgId, actorAuthMethod, actorId, actor, filter }: TListRolesDTO) => { - let projectId = ""; + let project: TProjects; if (filter.type === ProjectRoleServiceIdentifierType.SLUG) { - const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); - if (!project) throw new BadRequestError({ message: "Project not found" }); - projectId = project.id; + project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId); } else { - projectId = filter.projectId; + project = await projectDAL.findProjectById(filter.projectId); } + if (!project) throw new BadRequestError({ message: "Project not found" }); + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId, + projectId: project.id, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); const customRoles = await projectRoleDAL.find( - { projectId }, + { projectId: project.id }, { sort: [[`${TableName.ProjectRoles}.slug` as "slug", "asc"]] } ); - const roles = [...getPredefinedRoles(projectId), ...(customRoles || [])]; + const roles = [ + ...getPredefinedRoles({ projectId: project.id, projectType: project.type as ProjectType }), + ...(customRoles || []) + ]; return roles; }; diff --git a/backend/src/services/project-role/project-role-types.ts b/backend/src/services/project-role/project-role-types.ts index a71c73113..508623a0c 100644 --- a/backend/src/services/project-role/project-role-types.ts +++ b/backend/src/services/project-role/project-role-types.ts @@ -1,4 +1,4 @@ -import { TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectType, TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export enum ProjectRoleServiceIdentifierType { @@ -34,3 +34,9 @@ export type TListRolesDTO = { | { type: ProjectRoleServiceIdentifierType.SLUG; projectSlug: string } | { type: ProjectRoleServiceIdentifierType.ID; projectId: string }; } & Omit; + +export type TGetPredefinedRolesDTO = { + projectId: string; + projectType: ProjectType; + roleFilter?: ProjectMembershipRole; +}; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 8e60252ba..7ab45baa7 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -14,6 +14,7 @@ import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/servi import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, + ProjectPermissionCertificateActions, ProjectPermissionSecretActions, ProjectPermissionSshHostActions, ProjectPermissionSub @@ -328,14 +329,16 @@ export const projectServiceFactory = ({ // set default environments and root folder for provided environments let envs: TProjectEnvironments[] = []; if (projectTemplate) { - envs = await projectEnvDAL.insertMany( - projectTemplate.environments.map((env) => ({ ...env, projectId: project.id })), - tx - ); - await folderDAL.insertMany( - envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })), - tx - ); + if (projectTemplate.environments) { + envs = await projectEnvDAL.insertMany( + projectTemplate.environments.map((env) => ({ ...env, projectId: project.id })), + tx + ); + await folderDAL.insertMany( + envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })), + tx + ); + } await projectRoleDAL.insertMany( projectTemplate.packedRoles.map((role) => ({ ...role, @@ -591,7 +594,10 @@ export const projectServiceFactory = ({ workspaces.map(async (workspace) => { return { ...workspace, - roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles(workspace.id)] + roles: [ + ...(workspaceMappedToRoles[workspace.id] || []), + ...getPredefinedRoles({ projectId: workspace.id, projectType: workspace.type as ProjectType }) + ] }; }) ); @@ -948,7 +954,10 @@ export const projectServiceFactory = ({ actionProjectType: ActionProjectType.CertificateManager }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); const cas = await certificateAuthorityDAL.find({ projectId }); diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts index 7e77bd256..abc4dcf82 100644 --- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts @@ -169,7 +169,7 @@ const getParameterStoreTagsRecord = async ( throw new SecretSyncError({ message: - "IAM role has inadequate permissions to manage resource tags. Ensure the following polices are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource", + "IAM role has inadequate permissions to manage resource tags. Ensure the following policies are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource", shouldRetry: false }); } diff --git a/backend/src/services/super-admin/invalidate-cache-queue.ts b/backend/src/services/super-admin/invalidate-cache-queue.ts new file mode 100644 index 000000000..c2a12f5d5 --- /dev/null +++ b/backend/src/services/super-admin/invalidate-cache-queue.ts @@ -0,0 +1,49 @@ +import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +import { CacheType } from "./super-admin-types"; + +export type TInvalidateCacheQueueFactoryDep = { + queueService: TQueueServiceFactory; + + keyStore: Pick; +}; + +export type TInvalidateCacheQueueFactory = ReturnType; + +export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalidateCacheQueueFactoryDep) => { + const startInvalidate = async (dto: { + data: { + type: CacheType; + }; + }) => { + await queueService.queue(QueueName.InvalidateCache, QueueJobs.InvalidateCache, dto, { + removeOnComplete: true, + removeOnFail: true, + jobId: `invalidate-cache-${dto.data.type}` + }); + }; + + queueService.start(QueueName.InvalidateCache, async (job) => { + try { + const { + data: { type } + } = job.data; + + await keyStore.setItemWithExpiry("invalidating-cache", 1800, "true"); // 30 minutes max (in case the job somehow silently fails) + + if (type === CacheType.ALL || type === CacheType.SECRETS) + await keyStore.deleteItems({ pattern: "secret-manager:*" }); + + await keyStore.deleteItem("invalidating-cache"); + } catch (err) { + logger.error(err, "Failed to invalidate cache"); + await keyStore.deleteItem("invalidating-cache"); + } + }); + + return { + startInvalidate + }; +}; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 8687826c8..7c9ca4f38 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -25,8 +25,10 @@ import { TOrgServiceFactory } from "../org/org-service"; import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; +import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue"; import { TSuperAdminDALFactory } from "./super-admin-dal"; import { + CacheType, LoginMethod, TAdminBootstrapInstanceDTO, TAdminGetIdentitiesDTO, @@ -46,9 +48,10 @@ type TSuperAdminServiceFactoryDep = { kmsService: Pick; kmsRootConfigDAL: TKmsRootConfigDALFactory; orgService: Pick; - keyStore: Pick; + keyStore: Pick; licenseService: Pick; microsoftTeamsService: Pick; + invalidateCacheQueue: TInvalidateCacheQueueFactory; }; export type TSuperAdminServiceFactory = ReturnType; @@ -64,7 +67,7 @@ export let getServerCfg: () => Promise< const ADMIN_CONFIG_KEY = "infisical-admin-cfg"; const ADMIN_CONFIG_KEY_EXP = 60; // 60s -const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; +export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; export const superAdminServiceFactory = ({ serverCfgDAL, @@ -80,7 +83,8 @@ export const superAdminServiceFactory = ({ identityAccessTokenDAL, identityTokenAuthDAL, identityOrgMembershipDAL, - microsoftTeamsService + microsoftTeamsService, + invalidateCacheQueue }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -631,6 +635,16 @@ export const superAdminServiceFactory = ({ await kmsService.updateEncryptionStrategy(strategy); }; + const invalidateCache = async (type: CacheType) => { + await invalidateCacheQueue.startInvalidate({ + data: { type } + }); + }; + + const checkIfInvalidatingCache = async () => { + return (await keyStore.getItem("invalidating-cache")) !== null; + }; + return { initServerCfg, updateServerCfg, @@ -644,6 +658,8 @@ export const superAdminServiceFactory = ({ getConfiguredEncryptionStrategies, grantServerAdminAccessToUser, deleteIdentitySuperAdminAccess, - deleteUserSuperAdminAccess + deleteUserSuperAdminAccess, + invalidateCache, + checkIfInvalidatingCache }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 64ec92632..c804bed74 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -44,3 +44,8 @@ export enum LoginMethod { LDAP = "ldap", OIDC = "oidc" } + +export enum CacheType { + ALL = "all", + SECRETS = "secrets" +} diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index ab90a71d4..9e046cdbd 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -21,7 +21,8 @@ export enum PostHogEventTypes { IssueSshHostUserCert = "Issue SSH Host User Certificate", IssueSshHostHostCert = "Issue SSH Host Host Certificate", SignCert = "Sign PKI Certificate", - IssueCert = "Issue PKI Certificate" + IssueCert = "Issue PKI Certificate", + InvalidateCache = "Invalidate Cache" } export type TSecretModifiedEvent = { @@ -203,6 +204,13 @@ export type TIssueCertificateEvent = { }; }; +export type TInvalidateCacheEvent = { + event: PostHogEventTypes.InvalidateCache; + properties: { + userAgent?: string; + }; +}; + export type TPostHogEvent = { distinctId: string } & ( | TSecretModifiedEvent | TAdminInitEvent @@ -221,4 +229,5 @@ export type TPostHogEvent = { distinctId: string } & ( | TIssueSshHostHostCertEvent | TSignCertificateEvent | TIssueCertificateEvent + | TInvalidateCacheEvent ); diff --git a/docs/api-reference/endpoints/certificates/bundle.mdx b/docs/api-reference/endpoints/certificates/bundle.mdx new file mode 100644 index 000000000..60d37a2d8 --- /dev/null +++ b/docs/api-reference/endpoints/certificates/bundle.mdx @@ -0,0 +1,8 @@ +--- +title: "Get Certificate Bundle" +openapi: "GET /api/v1/pki/certificates/{serialNumber}/bundle" +--- + + + You must have the certificate `read-private-key` permission in order to call this endpoint. + diff --git a/docs/api-reference/endpoints/certificates/private-key.mdx b/docs/api-reference/endpoints/certificates/private-key.mdx new file mode 100644 index 000000000..d0b93e65c --- /dev/null +++ b/docs/api-reference/endpoints/certificates/private-key.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Certificate Private Key" +openapi: "GET /api/v1/pki/certificates/{serialNumber}/private-key" +--- diff --git a/docs/api-reference/endpoints/ldap-auth/attach.mdx b/docs/api-reference/endpoints/ldap-auth/attach.mdx new file mode 100644 index 000000000..512878887 --- /dev/null +++ b/docs/api-reference/endpoints/ldap-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/ldap-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/ldap-auth/login.mdx b/docs/api-reference/endpoints/ldap-auth/login.mdx new file mode 100644 index 000000000..737afb857 --- /dev/null +++ b/docs/api-reference/endpoints/ldap-auth/login.mdx @@ -0,0 +1,4 @@ +--- +title: "Login" +openapi: "POST /api/v1/auth/ldap-auth/login" +--- diff --git a/docs/api-reference/endpoints/ldap-auth/retrieve.mdx b/docs/api-reference/endpoints/ldap-auth/retrieve.mdx new file mode 100644 index 000000000..fe4974cde --- /dev/null +++ b/docs/api-reference/endpoints/ldap-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/ldap-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/ldap-auth/revoke.mdx b/docs/api-reference/endpoints/ldap-auth/revoke.mdx new file mode 100644 index 000000000..2ef0996fd --- /dev/null +++ b/docs/api-reference/endpoints/ldap-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/ldap-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/ldap-auth/update.mdx b/docs/api-reference/endpoints/ldap-auth/update.mdx new file mode 100644 index 000000000..74b54efd3 --- /dev/null +++ b/docs/api-reference/endpoints/ldap-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/ldap-auth/identities/{identityId}" +--- diff --git a/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx b/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx index 3f62a3b61..b6d1d691f 100644 --- a/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx +++ b/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx @@ -27,7 +27,7 @@ User identities can have metadata attributes assigned directly. These attributes #### Applying ABAC Policies with User Metadata -Attribute-based access controls are currently only available for polices defined on Secrets Manager projects. +Attribute-based access controls are currently only available for policies defined on Secrets Manager projects. You can set ABAC permissions to dynamically set access to environments, folders, secrets, and secret tags. diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index 02d9c863a..7ccc098cd 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -73,6 +73,61 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t + + + The Gateway can be installed via [Helm](https://helm.sh/). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications. + + For production deployments on Kubernetes, install the Gateway using the Infisical Helm chart: + + ### Install the latest Helm Chart repository + ```bash + helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + ``` + + ### Update the Helm Chart repository + ```bash + helm repo update + ``` + + ### Create a Kubernetes Secret with the gateway token + + Create a new Kubernetes secret containing the gateway token as the `TOKEN` key. You can optionally also set the `INFISICAL_API_URL` key to your Infisical instance URL. By default, `INFISICAL_API_URL` is set to `https://app.infisical.com`. + + + ```bash + kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN= + ``` + + + The secret name is `infisical-gateway-environment` by default. The `TOKEN` key is required, and the `INFISICAL_API_URL` key is optional. + + + ### Install the Infisical Gateway Helm Chart + ```bash + helm install infisical-gateway infisical-helm-charts/infisical-gateway + ``` + + ### Check the gateway logs + After installing the gateway, you can check the logs to ensure it's running as expected. + + ```bash + kubectl logs deployment/infisical-gateway + ``` + + You should see the following output which indicates the gateway is running as expected. + ```bash + $ kubectl logs deployment/infisical-gateway + INF Provided relay port 5349. Using TLS + INF Connected with relay + INF 10.0.101.112:56735 + INF Starting relay connection health check + INF Gateway started successfully + INF New connection from: 10.0.1.8:34051 + INF Gateway is reachable by Infisical + ``` + + + For development or testing, you can run the Gateway directly. Log in with your machine identity and start the Gateway in one command: ```bash diff --git a/docs/documentation/platform/identities/aws-auth.mdx b/docs/documentation/platform/identities/aws-auth.mdx index 494606ccd..1c853957b 100644 --- a/docs/documentation/platform/identities/aws-auth.mdx +++ b/docs/documentation/platform/identities/aws-auth.mdx @@ -62,7 +62,7 @@ access the Infisical API using the AWS Auth authentication method. - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/identities/azure-auth.mdx b/docs/documentation/platform/identities/azure-auth.mdx index 03d997ffb..9576c4d0f 100644 --- a/docs/documentation/platform/identities/azure-auth.mdx +++ b/docs/documentation/platform/identities/azure-auth.mdx @@ -62,7 +62,7 @@ access the Infisical API using the Azure Auth authentication method. - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/identities/gcp-auth.mdx b/docs/documentation/platform/identities/gcp-auth.mdx index 6573544de..17dc5acd9 100644 --- a/docs/documentation/platform/identities/gcp-auth.mdx +++ b/docs/documentation/platform/identities/gcp-auth.mdx @@ -68,7 +68,7 @@ access the Infisical API using the GCP ID Token authentication method. - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) @@ -237,7 +237,7 @@ access the Infisical API using the GCP IAM authentication method. - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/identities/jwt-auth.mdx b/docs/documentation/platform/identities/jwt-auth.mdx index 3dcf12b29..339138881 100644 --- a/docs/documentation/platform/identities/jwt-auth.mdx +++ b/docs/documentation/platform/identities/jwt-auth.mdx @@ -57,7 +57,7 @@ In the following steps, we explore how to create and use identities to access th - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/identities/kubernetes-auth.mdx b/docs/documentation/platform/identities/kubernetes-auth.mdx index 58069f09e..cfa0e861a 100644 --- a/docs/documentation/platform/identities/kubernetes-auth.mdx +++ b/docs/documentation/platform/identities/kubernetes-auth.mdx @@ -163,7 +163,7 @@ In the following steps, we explore how to create and use identities for your app - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/identities/ldap-auth/general.mdx b/docs/documentation/platform/identities/ldap-auth/general.mdx new file mode 100644 index 000000000..7fb2798c7 --- /dev/null +++ b/docs/documentation/platform/identities/ldap-auth/general.mdx @@ -0,0 +1,87 @@ +--- +title: General +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. + +## Guide + + + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. + + ![Create identity](/images/platform/identities/ldap/identities-org-create-identity.png) + + When creating an identity, you specify an organization level role for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. + + ![Create identity modal](/images/platform/identities/ldap/identities-org-create-identity-modal.png) + + Now input a few details for your new identity. Here's some guidance for each field: + + - Name (required): A friendly name for the identity. + - Role (required): A role from the Organization Roles tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. + + Once you've created an identity, you'll be redirected to a page where you can manage the identity. + + + + To configure LDAP auth for your identity, press the **Add Auth Method** button on the identity's page. + + ![Add auth method](/images/platform/identities/ldap/identities-org-add-auth-method.png) + + Now select **LDAP Auth** from the list of available auth methods for the identity. + + ![Select LDAP auth](/images/platform/identities/ldap/identities-org-add-auth-method-modal.png) + + + After selecting **LDAP Auth**, you'll see the form you need to fill out to configure LDAP auth for your identity. The following fields are available: + + - `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`. + - `User Search Filter`: Template used to construct the LDAP user search filter such as `(uid={{username}})`; use literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas. + - `Required Attributes`: A key/value pair of attributes that must be present in the LDAP user entry for them to be authenticated. As an example, if you set key `uid` to value `user1,user2,user3`, then only users with `uid` of `user1`, `user2`, or `user3` will be able to login with this identity. Each value is a comma separated list of attributes. + - `CA Certificate`: The CA certificate to use when verifying the LDAP server certificate. This field is optional but recommended. + - `Access Token TTL` _(default is 2592000 equivalent to 30 days)_: The lifetime for an access token in seconds. This value will be referenced at renewal time. + - `Access Token Max TTL` _(default is 2592000 equivalent to 30 days)_: The maximum lifetime for an access token in seconds. This value will be referenced at renewal time. + - `Access Token Max Number of Uses` _(default is 0)_: The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses. + - `Access Token Trusted IPs`: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address. + + Once you've filled out the form, press **Add** to save your changes. + + ![Configure LDAP auth](/images/platform/identities/ldap/identities-org-configure-ldap.png) + + + After configuring LDAP auth for your identity, you can authenticate with the identity and obtain an access token using your LDAP credentials. + + ```bash + curl --request POST \ + --url https://app.infisical.com/api/v1/auth/ldap-auth/login \ + --header 'Content-Type: application/json' \ + --data '{ + "identityId": "", + "username": "", + "password": "" + }' + ``` + + + For EU Cloud and Self-Hosted users, make sure to replace `https://app.infisical.com` with `https://eu.infisical.com` or your self-hosted instance's URL in the request URL. + + + If successful, you'll receive an access token in the response body. + + ```json + { + "accessToken": "your-access-token", + "expiresIn": 2592000, + "accessTokenMaxTTL": 2592000, + "tokenType": "Bearer" + } + ``` + + You can read more about the login API endpoint [here](/api-reference/endpoints/ldap-auth/login). + + + diff --git a/docs/documentation/platform/identities/ldap-auth/jumpcloud.mdx b/docs/documentation/platform/identities/ldap-auth/jumpcloud.mdx new file mode 100644 index 000000000..4bd497eac --- /dev/null +++ b/docs/documentation/platform/identities/ldap-auth/jumpcloud.mdx @@ -0,0 +1,97 @@ +--- +title: JumpCloud +description: "Learn how to authenticate with Infisical using LDAP with JumpCloud." +--- + +**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. + +## Guide + + + + In JumpCloud, head to USER MANAGEMENT > Users and create a new user via the Manual user entry option. + This user will be used as a privileged service account to facilitate Infisical's ability to bind/search the LDAP directory. + + Next after creating the user, under User Security Settings and Permissions > Permission Settings, check the box next to Enable as LDAP Bind DN. + + ![User management](/images/platform/identities/ldap/jumpcloud-users-management.png) + + + + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. + + ![Create identity](/images/platform/identities/ldap/identities-org-create-identity.png) + + When creating an identity, you specify an organization level role for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. + + ![Create identity modal](/images/platform/identities/ldap/identities-org-create-identity-modal.png) + + Now input a few details for your new identity. Here's some guidance for each field: + + - Name (required): A friendly name for the identity. + - Role (required): A role from the Organization Roles tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. + + Once you've created an identity, you'll be redirected to a page where you can manage the identity. + + + + To configure LDAP auth for your identity, press the **Add Auth Method** button on the identity's page. + + ![Add auth method](/images/platform/identities/ldap/identities-org-add-auth-method.png) + + Now select **LDAP Auth** from the list of available auth methods for the identity. + + ![Select LDAP auth](/images/platform/identities/ldap/identities-org-add-auth-method-modal.png) + + + After selecting **LDAP Auth**, you'll see the form you need to fill out to configure LDAP auth for your identity. The following fields are available: + + - `URL`: The LDAP server to connect to (`ldaps://ldap.jumpcloud.com:636`). + - `Bind DN`: The distinguished name of object to bind when performing the user search (`uid=,ou=Users,o=,dc=jumpcloud,dc=com`). + - `Bind Pass`: The password to use along with Bind DN when performing the user search. This is the password for the user created in the previous step. + - `Search Base / DN`: Base DN under which to perform user search (`ou=Users,o=,dc=jumpcloud,dc=com`). + - `User Search Filter`: Template used to construct the LDAP user search filter (`(uid={{username}})`). + - `Required Attributes`: A key/value pair of attributes that must be present in the LDAP user entry for them to be authenticated. As an example, if you set key `uid` to value `user1,user2,user3`, then only users with `uid` of `user1`, `user2`, or `user3` will be able to login with this identity. Each value is a comma separated list of attributes. + - `CA Certificate`: The CA certificate to use when verifying the LDAP server certificate (instructions to obtain the certificate for JumpCloud [here](https://jumpcloud.com/support/connect-to-ldap-with-tls-ssl)). + - `Access Token TTL` _(default is 2592000 equivalent to 30 days)_: The lifetime for an access token in seconds. This value will be referenced at renewal time. + - `Access Token Max TTL` _(default is 2592000 equivalent to 30 days)_: The maximum lifetime for an access token in seconds. This value will be referenced at renewal time. + - `Access Token Max Number of Uses` _(default is 0)_: The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses. + - `Access Token Trusted IPs`: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address. + + Once you've filled out the form, press **Add** to save your changes. + + ![Configure LDAP auth](/images/platform/identities/ldap/identities-org-configure-ldap.png) + + + After configuring LDAP auth for your identity, you can authenticate with the identity and obtain an access token using your LDAP credentials. + + ```bash + curl --request POST \ + --url https://app.infisical.com/api/v1/auth/ldap-auth/login \ + --header 'Content-Type: application/json' \ + --data '{ + "identityId": "", + "username": "", + "password": "" + }' + ``` + + + For EU Cloud and Self-Hosted users, make sure to replace `https://app.infisical.com` with `https://eu.infisical.com` or your self-hosted instance's URL in the request URL. + + + If successful, you'll receive an access token in the response body. + + ```json + { + "accessToken": "your-access-token", + "expiresIn": 2592000, + "accessTokenMaxTTL": 2592000, + "tokenType": "Bearer" + } + ``` + + You can read more about the login API endpoint [here](/api-reference/endpoints/ldap-auth/login). + + + diff --git a/docs/documentation/platform/identities/oidc-auth/circleci.mdx b/docs/documentation/platform/identities/oidc-auth/circleci.mdx index ddf74e3fa..6849b77f9 100644 --- a/docs/documentation/platform/identities/oidc-auth/circleci.mdx +++ b/docs/documentation/platform/identities/oidc-auth/circleci.mdx @@ -52,7 +52,7 @@ In the following steps, we explore how to create and use identities to access th - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/identities/oidc-auth/general.mdx b/docs/documentation/platform/identities/oidc-auth/general.mdx index 776d175a4..9a39adba3 100644 --- a/docs/documentation/platform/identities/oidc-auth/general.mdx +++ b/docs/documentation/platform/identities/oidc-auth/general.mdx @@ -56,7 +56,7 @@ In the following steps, we explore how to create and use identities to access th - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/identities/oidc-auth/github.mdx b/docs/documentation/platform/identities/oidc-auth/github.mdx index 47352a339..a377ac37c 100644 --- a/docs/documentation/platform/identities/oidc-auth/github.mdx +++ b/docs/documentation/platform/identities/oidc-auth/github.mdx @@ -55,7 +55,7 @@ In the following steps, we explore how to create and use identities to access th - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/identities/oidc-auth/gitlab.mdx b/docs/documentation/platform/identities/oidc-auth/gitlab.mdx index 228392aa6..b52d2f894 100644 --- a/docs/documentation/platform/identities/oidc-auth/gitlab.mdx +++ b/docs/documentation/platform/identities/oidc-auth/gitlab.mdx @@ -55,7 +55,7 @@ In the following steps, we explore how to create and use identities to access th - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/identities/token-auth.mdx b/docs/documentation/platform/identities/token-auth.mdx index 59c5f9abf..500adf509 100644 --- a/docs/documentation/platform/identities/token-auth.mdx +++ b/docs/documentation/platform/identities/token-auth.mdx @@ -38,7 +38,7 @@ using the Token Auth authentication method. - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index 4d66e30b4..30f1f10d2 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -42,7 +42,7 @@ using the Universal Auth authentication method. - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) diff --git a/docs/documentation/platform/project-templates.mdx b/docs/documentation/platform/project-templates.mdx index d84c6bdc1..7dd5ceb50 100644 --- a/docs/documentation/platform/project-templates.mdx +++ b/docs/documentation/platform/project-templates.mdx @@ -33,7 +33,7 @@ In the following steps, we'll explore how to set up a project template. - Navigate to the Project Templates tab on the Organization Settings page and tap on the **Add Template** button. + Navigate to the **Project Templates** tab on the Feature Settings page for the project type you want to create a template for and tap on the **Add Template** button. ![project template add button](/images/platform/project-templates/project-template-add-button.png) Specify your template details. Here's some guidance on each field: @@ -67,6 +67,7 @@ In the following steps, we'll explore how to set up a project template. --header 'Content-Type: application/json' \ --data '{ "name": "my-project-template", + "type": "secret-manager", "description": "...", "environments": "[...]", "roles": "[...]", diff --git a/docs/documentation/platform/secret-scanning.mdx b/docs/documentation/platform/secret-scanning.mdx index 4f030e882..da28bfa55 100644 --- a/docs/documentation/platform/secret-scanning.mdx +++ b/docs/documentation/platform/secret-scanning.mdx @@ -7,6 +7,113 @@ The Infisical Secret Scanner allows you to keep an overview and stay alert of ex To further enhance security, we recommend you also use our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to scan for exposed secrets prior to pushing your changes. + + + + To setup secret scanning on your own instance of Infisical, you can follow the steps below. + + + + Create a new GitHub app in your GitHub organization or personal [Developer Settings](https://github.com/settings/apps). + + ![Create GitHub App](/images/platform/secret-scanning/github-create-app.png) + + ### Configure the GitHub App + To configure the GitHub app to work with Infisical, you'll need to modify the following settings: + - **Homepage URL**: Required to be set. Set it to the URL of your Infisical instance. (e.g. `https://app.infisical.com`) + - **Setup URL**: Set this to `https:///organization/secret-scanning` + - **Webhook URL**: Set this to `https:///api/v1/secret-scanning/webhook` + - **Webhook Secret**: Set this to a random string. This is used to verify the webhook request from Infisical. Use `openssl rand -base64 32` in your terminal to generate a random secret. + + + Remember to save the webhook secret as you will need it in the next step. + + + ![GitHub App Settings](/images/platform/secret-scanning/github-configure-app.png) + + ### Configure the GitHub App Permissions + The GitHub app needs the following permissions: + + Repository permissions: + - `Checks`: Read and Write + - `Contents`: Read-only + - `Issues`: Read and Write + - `Pull Requests`: Read and Write + - `Metadata`: Read-only (enabled by default) + + ![Github App Repository Permissions](/images/platform/secret-scanning/github-repo-permissions.png) + + Subscribed events: + - `Check run` + - `Pull request` + - `Push` + + ![Github App Subscribed Events](/images/platform/secret-scanning/github-subscribed-events.png) + + + ### Create the GitHub App + Now you can create the GitHub app by clicking on the "Create GitHub App" button. + + + If you want other Github users to be able to install the app, you need to tick the "Any account" option under "Where can this GitHub App be installed?" + + + ![Create GitHub App](/images/platform/secret-scanning/github-create-app-button.png) + + + + After clicking the "Create GitHub App" button, you will be redirected to the GitHub settings page. Here you can copy the "App ID" and save it for later when you need to configure your environment variables for your Infisical instance. + + ![Github App ID](/images/platform/secret-scanning/github-app-copy-app-id.png) + + + + The GitHub App slug is the name of the app you created in a slug friendly format. You can find the slug in the URL of the app you created. + + ![Github App Slug](/images/platform/secret-scanning/github-app-copy-slug.png) + + + + Create a new app private key by clicking on the "Generate a private key" button under the "Private keys" section. + + Once you click the "Generate a private key" button, the private key will be downloaded to your computer. Save this file for later as you will need the private key when configuring Infisical. + + ![Github App Private Key](/images/platform/secret-scanning/github-app-create-private-key.png) + + + Remember to save the private key as you will need it in the next step. + + + + + + + Now you can configure your Infisical instance by setting the following environment variables: + + - `SECRET_SCANNING_GIT_APP_ID`: The App ID of your GitHub App. + - `SECRET_SCANNING_GIT_APP_SLUG`: The slug of your GitHub App. + - `SECRET_SCANNING_PRIVATE_KEY`: The private key of your GitHub App that you created in a previous step. + - `SECRET_SCANNING_WEBHOOK_SECRET`: The webhook secret of your GitHub App that you created in a previous step. + + + + After restarting your Infisical instance, you should be able to use the secret scanning feature within your organization. Follow the steps below to add the GitHub App to your Infisical organization. + + +## Install the Infisical Radar GitHub App + +To install the GitHub App, press the "Integrate With GitHub" button in the top right corner of your Infisical Secret Scanning dashboard. + +![Integrate With GitHub](/images/platform/secret-scanning/infisical-connect-secret-scanner.png) + +Next, you'll be prompted to select which organization you'd like to install the app into. Select the organization you'd like to install the app into by clicking the organization in the menu. + +![Select Organization](/images/platform/secret-scanning/github-select-org-2.png) + +Select the repositories you'd like to scan for secrets and press the "Install" button. + +![Select Repositories](/images/platform/secret-scanning/github-select-repos.png) + ## Code Scanning ![Scanning Overview](/images/platform/secret-scanning/overview.png) diff --git a/docs/documentation/platform/ssh/overview.mdx b/docs/documentation/platform/ssh/overview.mdx index e71eeabe1..a252e1f71 100644 --- a/docs/documentation/platform/ssh/overview.mdx +++ b/docs/documentation/platform/ssh/overview.mdx @@ -31,16 +31,9 @@ we will register a remote host with Infisical through a [machine identity](/docu - 1.1. Start by creating a new Infisical SSH project in Infisical. + Start by creating a new Infisical SSH project in Infisical. ![ssh project create](/images/platform/ssh/v2/ssh-create-project.png) - - 1.2. Create a custom role in the project under Access Control > Project Roles to grant the machine identity that we will create in step 2 the ability to **Create** and **Issue Host Certificates** on the **SSH Host** resource; this will enable the linked machine identity to bootstrap a remote host with Infisical - and establish the necessary configuration on it. - - ![ssh custom role bootstrap 1](/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png) - - ![ssh custom role bootstrap 2](/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png) 2.1. Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth. @@ -52,7 +45,14 @@ we will register a remote host with Infisical through a [machine identity](/docu You may use other authentication methods as suitable (e.g. [AWS Auth](/documentation/platform/identities/aws-auth), [Azure Auth](/documentation/platform/identities/azure-auth), [GCP Auth](/documentation/platform/identities/gcp-auth), etc.) as part of the machine identity configuration but, to keep this example simple, we will be using Universal Auth. - 2.2. Add the machine identity to the Infisical SSH project you created in the previous step and assign it the custom role you created in step 1.2. + 2.2. Add the machine identity to the Infisical SSH project you created in the previous step and assign it the **SSH Host Bootstrapper** role. + + This role grants the ability to **Create** and **Issue Host Certificates** on the **SSH Host** resource; this will enable the linked machine identity to bootstrap a remote host with Infisical + and establish the necessary configuration on it. + + + If you plan to use a custom role to bootstrap SSH hosts, ensure the role has the **Create** and **Issue Host Certificates** on the **SSH Host** resource. + ![ssh add identity to project](/images/platform/ssh/v2/ssh-add-identity-to-project.png) diff --git a/docs/images/platform/identities/ldap/identities-org-add-auth-method-modal.png b/docs/images/platform/identities/ldap/identities-org-add-auth-method-modal.png new file mode 100644 index 000000000..e9a5f276c Binary files /dev/null and b/docs/images/platform/identities/ldap/identities-org-add-auth-method-modal.png differ diff --git a/docs/images/platform/identities/ldap/identities-org-add-auth-method.png b/docs/images/platform/identities/ldap/identities-org-add-auth-method.png new file mode 100644 index 000000000..95d301010 Binary files /dev/null and b/docs/images/platform/identities/ldap/identities-org-add-auth-method.png differ diff --git a/docs/images/platform/identities/ldap/identities-org-configure-ldap.png b/docs/images/platform/identities/ldap/identities-org-configure-ldap.png new file mode 100644 index 000000000..c9dfb4950 Binary files /dev/null and b/docs/images/platform/identities/ldap/identities-org-configure-ldap.png differ diff --git a/docs/images/platform/identities/ldap/identities-org-create-identity-modal.png b/docs/images/platform/identities/ldap/identities-org-create-identity-modal.png new file mode 100644 index 000000000..3ac6555e4 Binary files /dev/null and b/docs/images/platform/identities/ldap/identities-org-create-identity-modal.png differ diff --git a/docs/images/platform/identities/ldap/identities-org-create-identity.png b/docs/images/platform/identities/ldap/identities-org-create-identity.png new file mode 100644 index 000000000..1086f6521 Binary files /dev/null and b/docs/images/platform/identities/ldap/identities-org-create-identity.png differ diff --git a/docs/images/platform/identities/ldap/jumpcloud-users-management.png b/docs/images/platform/identities/ldap/jumpcloud-users-management.png new file mode 100644 index 000000000..cc5dc13ca Binary files /dev/null and b/docs/images/platform/identities/ldap/jumpcloud-users-management.png differ diff --git a/docs/images/platform/project-templates/project-template-add-button.png b/docs/images/platform/project-templates/project-template-add-button.png index 965de1e9a..c71c5c938 100644 Binary files a/docs/images/platform/project-templates/project-template-add-button.png and b/docs/images/platform/project-templates/project-template-add-button.png differ diff --git a/docs/images/platform/project-templates/project-template-apply.png b/docs/images/platform/project-templates/project-template-apply.png index 1ec49cb43..0ed2d320c 100644 Binary files a/docs/images/platform/project-templates/project-template-apply.png and b/docs/images/platform/project-templates/project-template-apply.png differ diff --git a/docs/images/platform/project-templates/project-template-create.png b/docs/images/platform/project-templates/project-template-create.png index 6cd109049..6c485b4cc 100644 Binary files a/docs/images/platform/project-templates/project-template-create.png and b/docs/images/platform/project-templates/project-template-create.png differ diff --git a/docs/images/platform/project-templates/project-template-customized.png b/docs/images/platform/project-templates/project-template-customized.png index f21717326..182e669c2 100644 Binary files a/docs/images/platform/project-templates/project-template-customized.png and b/docs/images/platform/project-templates/project-template-customized.png differ diff --git a/docs/images/platform/project-templates/project-template-edit-form.png b/docs/images/platform/project-templates/project-template-edit-form.png index c4e29297f..72085468f 100644 Binary files a/docs/images/platform/project-templates/project-template-edit-form.png and b/docs/images/platform/project-templates/project-template-edit-form.png differ diff --git a/docs/images/platform/secret-scanning/github-app-copy-app-id.png b/docs/images/platform/secret-scanning/github-app-copy-app-id.png new file mode 100644 index 000000000..a94cb5ece Binary files /dev/null and b/docs/images/platform/secret-scanning/github-app-copy-app-id.png differ diff --git a/docs/images/platform/secret-scanning/github-app-copy-slug.png b/docs/images/platform/secret-scanning/github-app-copy-slug.png new file mode 100644 index 000000000..c555dcd41 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-app-copy-slug.png differ diff --git a/docs/images/platform/secret-scanning/github-app-create-private-key.png b/docs/images/platform/secret-scanning/github-app-create-private-key.png new file mode 100644 index 000000000..50f602a36 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-app-create-private-key.png differ diff --git a/docs/images/platform/secret-scanning/github-configure-app.png b/docs/images/platform/secret-scanning/github-configure-app.png new file mode 100644 index 000000000..df64eeb18 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-configure-app.png differ diff --git a/docs/images/platform/secret-scanning/github-create-app-button.png b/docs/images/platform/secret-scanning/github-create-app-button.png new file mode 100644 index 000000000..3ea4b2d38 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-create-app-button.png differ diff --git a/docs/images/platform/secret-scanning/github-create-app.png b/docs/images/platform/secret-scanning/github-create-app.png new file mode 100644 index 000000000..f4d1cdb8c Binary files /dev/null and b/docs/images/platform/secret-scanning/github-create-app.png differ diff --git a/docs/images/platform/secret-scanning/github-register-app.png b/docs/images/platform/secret-scanning/github-register-app.png new file mode 100644 index 000000000..904c07bf2 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-register-app.png differ diff --git a/docs/images/platform/secret-scanning/github-repo-permissions.png b/docs/images/platform/secret-scanning/github-repo-permissions.png new file mode 100644 index 000000000..53eae9a41 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-repo-permissions.png differ diff --git a/docs/images/platform/secret-scanning/github-select-org-2.png b/docs/images/platform/secret-scanning/github-select-org-2.png new file mode 100644 index 000000000..55b945c18 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-select-org-2.png differ diff --git a/docs/images/platform/secret-scanning/github-select-org.png b/docs/images/platform/secret-scanning/github-select-org.png new file mode 100644 index 000000000..7d6e5abc5 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-select-org.png differ diff --git a/docs/images/platform/secret-scanning/github-select-repos.png b/docs/images/platform/secret-scanning/github-select-repos.png new file mode 100644 index 000000000..51a6648d2 Binary files /dev/null and b/docs/images/platform/secret-scanning/github-select-repos.png differ diff --git a/docs/images/platform/secret-scanning/github-subscribed-events.png b/docs/images/platform/secret-scanning/github-subscribed-events.png new file mode 100644 index 000000000..7aa6b431f Binary files /dev/null and b/docs/images/platform/secret-scanning/github-subscribed-events.png differ diff --git a/docs/images/platform/secret-scanning/infisical-connect-secret-scanner.png b/docs/images/platform/secret-scanning/infisical-connect-secret-scanner.png new file mode 100644 index 000000000..11f24fd74 Binary files /dev/null and b/docs/images/platform/secret-scanning/infisical-connect-secret-scanner.png differ diff --git a/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png b/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png deleted file mode 100644 index 8acc1efe9..000000000 Binary files a/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png and /dev/null differ diff --git a/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png b/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png deleted file mode 100644 index 2ad9804d4..000000000 Binary files a/docs/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png and /dev/null differ diff --git a/docs/images/platform/ssh/v2/ssh-add-identity-to-project.png b/docs/images/platform/ssh/v2/ssh-add-identity-to-project.png index 83bd3c984..d921cc8d0 100644 Binary files a/docs/images/platform/ssh/v2/ssh-add-identity-to-project.png and b/docs/images/platform/ssh/v2/ssh-add-identity-to-project.png differ diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx index 21f54994a..5962e4c10 100644 --- a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -165,7 +165,7 @@ spec: - Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. + Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. #### Available options diff --git a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx index 50f07bb76..d87648bbf 100644 --- a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx @@ -34,7 +34,7 @@ Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes sec metadata: name: infisical-push-secret-demo spec: - resyncInterval: 1m + resyncInterval: 1m # Remove this field to disable automatic reconciliation of the InfisicalPushSecret CRD. hostAPI: https://app.infisical.com/api # Optional, defaults to no replacement. @@ -124,7 +124,9 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y - The `resyncInterval` is a string-formatted duration that defines the time between each resync. + The `resyncInterval` is a string-formatted duration that defines the time between each resync. The field is optional, and will default to no automatic resync if not defined. + + If you don't want to automatically reconcile the InfisicalPushSecret CRD on an interval, you can remove the `resyncInterval` field entirely from your InfisicalPushSecret CRD. The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. @@ -239,7 +241,21 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y DATABASE_URL: postgres://127.0.0.1:5432 ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab ``` + + + The `generators[]` field is used to define the generators you want to use for your InfisicalPushSecret CRD. + You can follow the guide for [using generators to push secrets](#using-generators-to-push-secrets) for more information. + Example: + + ```yaml + push: + generators: + - destinationSecretName: password-generator-test + generatorRef: + kind: Password + name: password-generator + ``` @@ -459,6 +475,148 @@ Using Go templates, you can format, combine, and create new key-value pairs of s Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information. +## Using generators to push secrets + +Generators allow secrets to be dynamically generated during each reconciliation cycle and then pushed to Infisical. They are useful for use cases where a new secret value is needed on every sync, such as ephemeral credentials or one-time-use tokens. + +A generator is defined as a custom resource (`ClusterGenerator`) within the cluster, which specifies the logic for generating secret values. Generators are stateless, each invocation triggers the creation of a new set of values, with no tracking or persistence of previously generated data. + +Because of this behavior, you may want to disable automatic syncing for the `InfisicalPushSecret` resource to avoid continuous regeneration of secrets. This can be done by omitting the `resyncInterval` field from the InfisicalPushSecret CRD. + +### Example usage +```yaml + push: + secret: + secretName: push-secret-source-secret + secretNamespace: dev + generators: + - destinationSecretName: password-generator # Name of the secret that will be created in Infisical + generatorRef: + kind: Password # Kind of the resource, must match the generator kind. + name: custom-generator # Name of the generator resource +``` + +To use a generator, you must specify at least one generator in the `push.generators[]` field. + + + + This field holds an array of the generators you want to use for your InfisicalPushSecret CRD. + + + + The name of the secret that will be created in Infisical. + + + + The reference to the generator resource. + + Valid fields: + - `kind`: The kind of the generator resource, must match the generator kind. + - `name`: The name of the generator resource. + + + + The kind of the generator resource, must match the generator kind. + + Valid values: + - `Password` + - `UUID` + + + + The name of the generator resource. + + +### Supported Generators +Below are the currently supported generators for the InfisicalPushSecret CRD. Each generator is a `ClusterGenerator` custom resource that can be used to customize the generated secret. + + + ### Password Generator + + The Password generator is a custom resource that is installed on the cluster that defines the logic for generating a password. + - `kind`: The kind of the generator resource, must match the generator kind. For the Password generator, the kind is `Password`. + - `generator.passwordSpec`: The spec of the password generator. + + + The `generator.kind` field must match the kind of the generator resource. For the Password generator, the kind should always be set to `Password`. + + + - `length`: The length of the password. + - `digits`: The number of digits in the password. + - `symbols`: The number of symbols in the password. + - `symbolCharacters`: The characters to use for the symbols in the password. + - `noUpper`: Whether to include uppercase letters in the password. + - `allowRepeat`: Whether to allow repeating characters in the password. + + + ```yaml password-cluster-generator.yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: ClusterGenerator + metadata: + name: password-generator + spec: + kind: Password + generator: + passwordSpec: + length: 10 + digits: 5 + symbols: 5 + symbolCharacters: "-_$@" + noUpper: false + allowRepeat: true + ``` + + Example InfisicalPushSecret CRD using the Password generator: + ```yaml infisical-push-secret-crd.yaml + push: + generators: + - destinationSecretName: password-generator-test + generatorRef: + kind: Password + name: password-generator + ``` + + + ### UUID Generator + + The UUID generator is a custom resource that is installed on the cluster that defines the logic for generating a UUID. + - `kind`: The kind of the generator resource, must match the generator kind. For the UUID generator, the kind is `UUID`. + - `generator.uuidSpec`: The spec of the UUID generator. For UUID's, this can be left empty. + + + The `generator.kind` field must match the kind of the generator resource. For the UUID generator, the kind should always be set to `UUID`. + + + + The spec of the UUID generator. For UUID's, this can be left empty. + + + ```yaml uuid-cluster-generator.yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: ClusterGenerator + metadata: + name: uuid-generator + spec: + kind: UUID + generator: + uuidSpec: + ``` + + Example InfisicalPushSecret CRD using the UUID generator: + + ```yaml infisical-push-secret-crd.yaml + push: + generators: + - destinationSecretName: uuid-generator-test + generatorRef: + kind: UUID + name: uuid-generator + ``` + + + + + ## Applying the InfisicalPushSecret CRD to your cluster Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster. diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx index a79a8d0a5..145737e96 100644 --- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -232,7 +232,7 @@ spec: - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) @@ -407,7 +407,7 @@ spec: - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + To create an identity, head to your Organization Settings > Access Control > Identities and press **Create identity**. ![identities organization](/images/platform/identities/identities-org.png) @@ -832,7 +832,7 @@ The namespace of the managed Kubernetes secret to be created. Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. -Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. +Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. #### Available options @@ -940,7 +940,7 @@ The Infisical operator will automatically create the Kubernetes config map in th The namespace of the managed Kubernetes config map that your Infisical data will be stored in. - Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator. + Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. #### Available options diff --git a/docs/internals/bug-bounty.mdx b/docs/internals/bug-bounty.mdx index b823e6246..e45de05bf 100644 --- a/docs/internals/bug-bounty.mdx +++ b/docs/internals/bug-bounty.mdx @@ -41,7 +41,7 @@ All final reward amounts are determined at Infisical's discretion based on impac ### Out of Scope -- Social engineering or phishing +- Social engineering or phishing (including email hyperlink injection without code execution) - Rate limiting issues on non-sensitive endpoints - Denial-of-service attacks that require authentication and don't impact core service availability - Findings based on outdated or forked code not maintained by the Infisical team @@ -57,4 +57,24 @@ We ask that researchers: - Use testing accounts where possible - Give us a reasonable window to investigate and patch before going public -Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally. \ No newline at end of file +Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally. + +### Program Conduct and Enforcement + +We value professional and collaborative interaction with security researchers. To maintain the integrity of our bug bounty program, we expect all participants to adhere to the following guidelines: + +- Maintain professional communication in all interactions +- Do not threaten public disclosure of vulnerabilities before we've had reasonable time to investigate and address the issue +- Do not attempt to extort or coerce compensation through threats +- Follow the responsible disclosure process outlined in this document +- Do not use automated scanning tools without prior permission + +Violations of these guidelines may result in: + +1. **Warning**: For minor violations, we may issue a warning explaining the violation and requesting compliance with program guidelines. +2. **Temporary Ban**: Repeated minor violations or more serious violations may result in a temporary suspension from the program. +3. **Permanent Ban**: Severe violations such as threats, extortion attempts, or unauthorized public disclosure will result in permanent removal from the Infisical Bug Bounty Program. + +We reserve the right to reject reports, withhold bounties, and remove participants from the program at our discretion for conduct that undermines the collaborative spirit of security research. + +Infisical is committed to working respectfully with security researchers who follow these guidelines, and we strive to recognize and reward valuable contributions that help protect our platform and users. diff --git a/docs/internals/permissions/project-permissions.mdx b/docs/internals/permissions/project-permissions.mdx index 4e0c592cb..acf95485b 100644 --- a/docs/internals/permissions/project-permissions.mdx +++ b/docs/internals/permissions/project-permissions.mdx @@ -252,11 +252,12 @@ Supports conditions and permission inversion #### Subject: `certificates` -| Action | Description | -| -------- | ----------------------------- | -| `read` | View certificates | -| `create` | Issue new certificates | -| `delete` | Revoke or remove certificates | +| Action | Description | +| -------------------- | ----------------------------- | +| `read` | View certificates | +| `read-private-key` | Read certificate private key | +| `create` | Issue new certificates | +| `delete` | Revoke or remove certificates | #### Subject: `certificate-templates` diff --git a/docs/mint.json b/docs/mint.json index a25a70124..ecfe798e2 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -247,68 +247,88 @@ { "group": "Authentication Methods", "pages": [ - "documentation/platform/auth-methods/email-password", - "documentation/platform/token", - "documentation/platform/identities/token-auth", - "documentation/platform/identities/universal-auth", - "documentation/platform/identities/kubernetes-auth", - "documentation/platform/identities/gcp-auth", - "documentation/platform/identities/azure-auth", - "documentation/platform/identities/aws-auth", - "documentation/platform/identities/jwt-auth", { - "group": "OIDC Auth", + "group": "User Authentication", "pages": [ - "documentation/platform/identities/oidc-auth/general", - "documentation/platform/identities/oidc-auth/github", - "documentation/platform/identities/oidc-auth/circleci", - "documentation/platform/identities/oidc-auth/gitlab", - "documentation/platform/identities/oidc-auth/terraform-cloud" - ] - }, - "documentation/platform/mfa", - { - "group": "SSO", - "pages": [ - "documentation/platform/sso/overview", - "documentation/platform/sso/google", - "documentation/platform/sso/github", - "documentation/platform/sso/gitlab", - "documentation/platform/sso/okta", - "documentation/platform/sso/azure", - "documentation/platform/sso/jumpcloud", - "documentation/platform/sso/keycloak-saml", - "documentation/platform/sso/google-saml", - "documentation/platform/sso/auth0-saml", + "documentation/platform/auth-methods/email-password", { - "group": "Keycloak OIDC", + "group": "SSO", "pages": [ - "documentation/platform/sso/keycloak-oidc/overview", - "documentation/platform/sso/keycloak-oidc/group-membership-mapping" + "documentation/platform/sso/overview", + "documentation/platform/sso/google", + "documentation/platform/sso/github", + "documentation/platform/sso/gitlab", + "documentation/platform/sso/okta", + "documentation/platform/sso/azure", + "documentation/platform/sso/jumpcloud", + "documentation/platform/sso/keycloak-saml", + "documentation/platform/sso/google-saml", + "documentation/platform/sso/auth0-saml", + { + "group": "Keycloak OIDC", + "pages": [ + "documentation/platform/sso/keycloak-oidc/overview", + "documentation/platform/sso/keycloak-oidc/group-membership-mapping" + ] + }, + "documentation/platform/sso/auth0-oidc", + "documentation/platform/sso/general-oidc" ] }, - "documentation/platform/sso/auth0-oidc", - "documentation/platform/sso/general-oidc" + { + "group": "LDAP", + "pages": [ + "documentation/platform/ldap/overview", + "documentation/platform/ldap/jumpcloud", + "documentation/platform/ldap/general" + ] + }, + { + "group": "SCIM", + "pages": [ + "documentation/platform/scim/overview", + "documentation/platform/scim/okta", + "documentation/platform/scim/azure", + "documentation/platform/scim/jumpcloud", + "documentation/platform/scim/group-mappings" + ] + } ] }, + { - "group": "LDAP", + "group": "Machine Identities", "pages": [ - "documentation/platform/ldap/overview", - "documentation/platform/ldap/jumpcloud", - "documentation/platform/ldap/general" - ] - }, - { - "group": "SCIM", - "pages": [ - "documentation/platform/scim/overview", - "documentation/platform/scim/okta", - "documentation/platform/scim/azure", - "documentation/platform/scim/jumpcloud", - "documentation/platform/scim/group-mappings" + "documentation/platform/identities/token-auth", + "documentation/platform/identities/universal-auth", + "documentation/platform/identities/kubernetes-auth", + "documentation/platform/identities/gcp-auth", + "documentation/platform/identities/azure-auth", + "documentation/platform/identities/aws-auth", + "documentation/platform/identities/jwt-auth", + + { + "group": "OIDC Auth", + "pages": [ + "documentation/platform/identities/oidc-auth/general", + "documentation/platform/identities/oidc-auth/github", + "documentation/platform/identities/oidc-auth/circleci", + "documentation/platform/identities/oidc-auth/gitlab", + "documentation/platform/identities/oidc-auth/terraform-cloud" + ] + }, + + { + "group": "LDAP Auth", + "pages": [ + "documentation/platform/identities/ldap-auth/general", + "documentation/platform/identities/ldap-auth/jumpcloud" + ] + } ] }, + "documentation/platform/token", + "documentation/platform/mfa", "documentation/platform/github-org-sync" ] }, @@ -715,6 +735,16 @@ "api-reference/endpoints/jwt-auth/revoke" ] }, + { + "group": "LDAP Auth", + "pages": [ + "api-reference/endpoints/ldap-auth/login", + "api-reference/endpoints/ldap-auth/attach", + "api-reference/endpoints/ldap-auth/retrieve", + "api-reference/endpoints/ldap-auth/update", + "api-reference/endpoints/ldap-auth/revoke" + ] + }, { "group": "Groups", "pages": [ @@ -1454,6 +1484,8 @@ "api-reference/endpoints/certificates/revoke", "api-reference/endpoints/certificates/delete", "api-reference/endpoints/certificates/cert-body", + "api-reference/endpoints/certificates/bundle", + "api-reference/endpoints/certificates/private-key", "api-reference/endpoints/certificates/issue-certificate", "api-reference/endpoints/certificates/sign-certificate" ] diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index d9eef9cb0..b63c58d3a 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -29,6 +29,19 @@ Used to configure platform-specific security and operational settings Specifies the internal port on which the application listens. + + Specifies the network interface Infisical will bind to when accepting incoming connections. + + By default, Infisical binds to `localhost`, which restricts access to connections from the same machine. + + To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces. + + Example values: + - `localhost` (default, same as `127.0.0.1`) + - `0.0.0.0` (all interfaces, accessible externally) + - `192.168.1.100` (specific interface IP) + + Telemetry helps us improve Infisical but if you want to disable it you may set this to `false`. @@ -612,6 +625,26 @@ To help you sync secrets from Infisical to services such as Github and Gitlab, I +## Secret Scanning + + + + The App ID of your GitHub App. + + + + The slug of your GitHub App. + + + + A private key for your GitHub App. + + + + The webhook secret of your GitHub App. + + + ## Observability You can configure Infisical to collect and expose telemetry data for analytics and monitoring. diff --git a/frontend/public/images/project-templates/project-templates-new-location.png b/frontend/public/images/project-templates/project-templates-new-location.png new file mode 100644 index 000000000..dd08f19a6 Binary files /dev/null and b/frontend/public/images/project-templates/project-templates-new-location.png differ diff --git a/frontend/src/components/projects/NewProjectModal.tsx b/frontend/src/components/projects/NewProjectModal.tsx index dcd3040d8..c98118a96 100644 --- a/frontend/src/components/projects/NewProjectModal.tsx +++ b/frontend/src/components/projects/NewProjectModal.tsx @@ -72,7 +72,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => { OrgPermissionSubjects.ProjectTemplates ); - const { data: projectTemplates = [] } = useListProjectTemplates({ + const { data: projectTemplates = [] } = useListProjectTemplates(projectType, { enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates) }); diff --git a/frontend/src/components/projects/ProjectSettings/ProjectSettings.tsx b/frontend/src/components/projects/ProjectSettings/ProjectSettings.tsx new file mode 100644 index 000000000..3919ad86c --- /dev/null +++ b/frontend/src/components/projects/ProjectSettings/ProjectSettings.tsx @@ -0,0 +1,30 @@ +import { useState } from "react"; + +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; + +import { ProjectTemplatesTab } from "./components"; + +const tabs = [ + { name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab } +]; + +export const ProjectSettings = () => { + const [selectedTab, setSelectedTab] = useState(tabs[0].key); + + return ( + + + {tabs.map((tab) => ( + + {tab.name} + + ))} + + {tabs.map(({ key, component: Component }) => ( + + + + ))} + + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/ProjectTemplatesTab.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/ProjectTemplatesTab.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/ProjectTemplatesTab.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/ProjectTemplatesTab.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/DeleteProjectTemplateModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx similarity index 92% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx index 98a675940..9b7164f80 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx @@ -7,6 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { usePopUp } from "@app/hooks"; import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal"; import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm"; @@ -24,7 +25,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa "editDetails" ] as const); - const { id: templateId, name, description } = projectTemplate; + const { id: templateId, name, description, type } = projectTemplate; const deleteProjectTemplate = useDeleteProjectTemplate(); @@ -94,10 +95,12 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa )} - + {type === ProjectType.SecretManager && ( + + )} { - const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); + const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const); const formMethods = useForm({ values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined, @@ -119,34 +119,29 @@ export const ProjectTemplateEditRoleForm = ({ - handlePopUpToggle("createPolicy", isOpen)} + - - - handlePopUpToggle("createPolicy")} /> - - + Add Policies + + handlePopUpToggle("addPolicy", isOpen)} + /> )} diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx similarity index 93% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx index b72d12c73..ef2691d69 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx @@ -19,7 +19,7 @@ import { THead, Tr } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates"; import { slugSchema } from "@app/lib/schemas"; @@ -35,6 +35,7 @@ const formSchema = z.object({ slug: slugSchema({ min: 1, max: 32 }) }) .array() + .nullish() }); type TFormSchema = z.infer; @@ -55,6 +56,8 @@ export const ProjectTemplateEnvironmentsForm = ({ resolver: zodResolver(formSchema) }); + const { subscription } = useSubscription(); + const { fields: environments, move, @@ -67,7 +70,7 @@ export const ProjectTemplateEnvironmentsForm = ({ const onFormSubmit = async (form: TFormSchema) => { try { const { environments: updatedEnvs } = await updateProjectTemplate.mutateAsync({ - environments: form.environments.map((env, index) => ({ + environments: form.environments?.map((env, index) => ({ ...env, position: index + 1 })), @@ -89,6 +92,9 @@ export const ProjectTemplateEnvironmentsForm = ({ } }; + const isEnvironmentLimitExceeded = + Boolean(subscription.environmentLimit) && environments.length >= subscription.environmentLimit; + return (
{(isAllowed) => ( diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/index.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/index.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/index.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/index.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/index.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/index.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/index.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/EditProjectTemplateSection/index.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx similarity index 91% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx index e601e0319..5b5728dac 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplateDetailsModal.tsx @@ -12,6 +12,7 @@ import { ModalContent, TextArea } from "@app/components/v2"; +import { useGetProjectTypeFromRoute } from "@app/hooks"; import { TProjectTemplate, useCreateProjectTemplate, @@ -41,6 +42,7 @@ type FormProps = { const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => { const createProjectTemplate = useCreateProjectTemplate(); const updateProjectTemplate = useUpdateProjectTemplate(); + const projectType = useGetProjectTypeFromRoute(); const { handleSubmit, @@ -55,9 +57,17 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => { }); const onFormSubmit = async (data: FormData) => { + if (!projectType) { + createNotification({ + text: "Failed to determine project type", + type: "error" + }); + return; + } + const mutation = projectTemplate ? updateProjectTemplate.mutateAsync({ templateId: projectTemplate.id, ...data }) - : createProjectTemplate.mutateAsync(data); + : createProjectTemplate.mutateAsync({ ...data, type: projectType }); try { const template = await mutation; createNotification({ diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx similarity index 98% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx index d4a076930..ec3f0aaff 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx @@ -50,7 +50,7 @@ export const ProjectTemplatesSection = () => { className="absolute min-h-[10rem] w-full" >
-

+

Create and configure templates with predefined roles and environments to streamline project setup

diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx similarity index 78% rename from frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx rename to frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx index b8dc00ab2..7448249ec 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx +++ b/frontend/src/components/projects/ProjectSettings/components/ProjectTemplatesTab/components/ProjectTemplatesTable.tsx @@ -23,7 +23,7 @@ import { Tr } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; -import { usePopUp } from "@app/hooks"; +import { useGetProjectTypeFromRoute, usePopUp } from "@app/hooks"; import { TProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; import { DeleteProjectTemplateModal } from "./DeleteProjectTemplateModal"; @@ -35,8 +35,10 @@ type Props = { export const ProjectTemplatesTable = ({ onEdit }: Props) => { const { subscription } = useSubscription(); - const { isPending, data: projectTemplates = [] } = useListProjectTemplates({ - enabled: subscription?.projectTemplates + const projectType = useGetProjectTypeFromRoute(); + + const { isPending, data: projectTemplates = [] } = useListProjectTemplates(projectType, { + enabled: subscription?.projectTemplates && Boolean(projectType) }); const [search, setSearch] = useState(""); @@ -50,6 +52,8 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => { [search, projectTemplates] ); + const isSecretManagerTemplates = projectType === "secret-manager"; + return (
{ Name Roles - Environments + {isSecretManagerTemplates && Environments} @@ -77,7 +81,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => { /> )} {filteredTemplates.map((template) => { - const { id, name, roles, environments, description } = template; + const { id, name, roles, environments = [], description } = template; return ( onEdit(template)} @@ -116,28 +120,30 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => { )} - - {environments.length} - {environments.length > 0 && ( - - {environments - .sort((a, b) => (a.position > b.position ? 1 : -1)) - .map((env) => ( -
  • {env.name}
  • - ))} - - } - > - -
    - )} - + {isSecretManagerTemplates && environments && ( + + {environments.length} + {environments.length > 0 && ( + + {environments + .sort((a, b) => (a.position > b.position ? 1 : -1)) + .map((env) => ( +
  • {env.name}
  • + ))} + + } + > + +
    + )} + + )} {name !== "default" && ( !Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole); -export const formatProjectRoleName = (name: string) => { - if (name === ProjectMemberRole.Member) return "developer"; - return name; -}; - export const isCustomProjectRole = (slug: string) => !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); @@ -28,3 +16,24 @@ export const findOrgMembershipRole = (roles: TOrgRole[], roleIdOrSlug: string) = isCustomOrgRole(roleIdOrSlug) ? roles.find((r) => r.id === roleIdOrSlug) : roles.find((r) => r.slug === roleIdOrSlug); + +export const formatProjectRoleName = (role: string, customRoleName?: string) => { + switch (role) { + case ProjectMembershipRole.Admin: + return "Admin"; + case ProjectMembershipRole.Member: + return "Developer"; + case ProjectMembershipRole.Viewer: + return "Viewer"; + case ProjectMembershipRole.NoAccess: + return "No Access"; + case ProjectMembershipRole.Custom: + return customRoleName ?? role; + case ProjectMembershipRole.SshHostBootstrapper: + return "SSH Host Bootstrapper"; + case ProjectMembershipRole.KmsCryptographicOperator: + return "Cryptographic Operator"; + default: + return role; + } +}; diff --git a/frontend/src/hooks/api/admin/index.ts b/frontend/src/hooks/api/admin/index.ts index bc812e18e..5bedf1158 100644 --- a/frontend/src/hooks/api/admin/index.ts +++ b/frontend/src/hooks/api/admin/index.ts @@ -3,6 +3,7 @@ export { useAdminGrantServerAdminAccess, useAdminRemoveIdentitySuperAdminAccess, useCreateAdminUser, + useInvalidateCache, useRemoveUserServerAdminAccess, useUpdateServerConfig, useUpdateServerEncryptionStrategy diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 2c88d4fd8..f220573c9 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -8,6 +8,7 @@ import { adminQueryKeys, adminStandaloneKeys } from "./queries"; import { RootKeyEncryptionStrategy, TCreateAdminUserDTO, + TInvalidateCacheDTO, TServerConfig, TUpdateServerConfigDTO } from "./types"; @@ -126,3 +127,15 @@ export const useUpdateServerEncryptionStrategy = () => { } }); }; + +export const useInvalidateCache = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (dto) => { + await apiRequest.post("/api/v1/admin/invalidate-cache", dto); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInvalidateCache() }); + } + }); +}; diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts index 1d44a93d0..85c6c153e 100644 --- a/frontend/src/hooks/api/admin/queries.ts +++ b/frontend/src/hooks/api/admin/queries.ts @@ -8,6 +8,7 @@ import { AdminGetIdentitiesFilters, AdminGetUsersFilters, AdminIntegrationsConfig, + TGetInvalidatingCacheStatus, TGetServerRootKmsEncryptionDetails, TServerConfig } from "./types"; @@ -22,8 +23,10 @@ export const adminQueryKeys = { getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const, getIdentities: (filters: AdminGetIdentitiesFilters) => [adminStandaloneKeys.getIdentities, { filters }] as const, - getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const, - getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const + getAdminSlackConfig: () => ["admin-slack-config"] as const, + getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const, + getInvalidateCache: () => ["admin-invalidate-cache"] as const, + getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const }; export const fetchServerConfig = async () => { @@ -118,3 +121,18 @@ export const useGetServerRootKmsEncryptionDetails = () => { } }); }; + +export const useGetInvalidatingCacheStatus = (enabled = true) => { + return useQuery({ + queryKey: adminQueryKeys.getInvalidateCache(), + queryFn: async () => { + const { data } = await apiRequest.get( + "/api/v1/admin/invalidating-cache-status" + ); + + return data.invalidating; + }, + enabled, + refetchInterval: (data) => (data ? 3000 : false) + }); +}; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 4533b5963..8850b3375 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -24,6 +24,7 @@ export type TServerConfig = { enabledLoginMethods: LoginMethod[]; authConsentContent?: string; pageFrameContent?: string; + invalidatingCache: boolean; }; export type TUpdateServerConfigDTO = { @@ -84,3 +85,16 @@ export enum RootKeyEncryptionStrategy { Software = "SOFTWARE", HSM = "HSM" } + +export enum CacheType { + ALL = "all", + SECRETS = "secrets" +} + +export type TInvalidateCacheDTO = { + type: CacheType; +}; + +export type TGetInvalidatingCacheStatus = { + invalidating: boolean; +}; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 9de5c4085..f726566cd 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -72,6 +72,8 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.DELETE_CERT]: "Delete certificate", [EventType.REVOKE_CERT]: "Revoke certificate", [EventType.GET_CERT_BODY]: "Get certificate body", + [EventType.GET_CERT_PRIVATE_KEY]: "Get certificate private key", + [EventType.GET_CERT_BUNDLE]: "Get certificate bundle", [EventType.CREATE_PKI_ALERT]: "Create PKI alert", [EventType.GET_PKI_ALERT]: "Get PKI alert", [EventType.UPDATE_PKI_ALERT]: "Update PKI alert", @@ -180,10 +182,17 @@ export const eventToNameMap: { [K in EventType]: string } = { "Microsoft Teams Workflow Integration Check Installation Status", [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS]: "Get Microsoft Teams tenant teams", [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET]: "Get Microsoft Teams Workflow Integration", - [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST]: "List Microsoft Teams Workflow Integration" + [EventType.MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST]: + "List Microsoft Teams Workflow Integration", + + [EventType.LOGIN_IDENTITY_LDAP_AUTH]: "Identity login via LDAP Auth", + [EventType.ADD_IDENTITY_LDAP_AUTH]: "Attached LDAP Auth to identity", + [EventType.UPDATE_IDENTITY_LDAP_AUTH]: "Updated LDAP Auth for identity", + [EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity", + [EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity" }; -export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { +export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = { [UserAgentType.WEB]: "Web", [UserAgentType.CLI]: "CLI", [UserAgentType.K8_OPERATOR]: "K8s operator", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 08f2559f6..b74969d6d 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -47,6 +47,13 @@ export enum EventType { CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", + + LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth", + ADD_IDENTITY_LDAP_AUTH = "add-identity-ldap-auth", + UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth", + GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth", + REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth", + CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", @@ -78,6 +85,8 @@ export enum EventType { DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", GET_CERT_BODY = "get-cert-body", + GET_CERT_PRIVATE_KEY = "get-cert-private-key", + GET_CERT_BUNDLE = "get-cert-bundle", CREATE_PKI_ALERT = "create-pki-alert", GET_PKI_ALERT = "get-pki-alert", UPDATE_PKI_ALERT = "update-pki-alert", diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 440f25c3d..745d0368f 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -620,6 +620,24 @@ interface GetCertBody { }; } +interface GetCertPrivateKey { + type: EventType.GET_CERT_PRIVATE_KEY; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + +interface GetCertBundle { + type: EventType.GET_CERT_BUNDLE; + metadata: { + certId: string; + cn: string; + serialNumber: string; + }; +} + interface CreatePkiAlert { type: EventType.CREATE_PKI_ALERT; metadata: { @@ -881,6 +899,8 @@ export type Event = | DeleteCert | RevokeCert | GetCertBody + | GetCertPrivateKey + | GetCertBundle | CreatePkiAlert | GetPkiAlert | UpdatePkiAlert diff --git a/frontend/src/hooks/api/certificates/queries.tsx b/frontend/src/hooks/api/certificates/queries.tsx index 50c751c06..c53cef471 100644 --- a/frontend/src/hooks/api/certificates/queries.tsx +++ b/frontend/src/hooks/api/certificates/queries.tsx @@ -6,7 +6,8 @@ import { TCertificate } from "./types"; export const certKeys = { getCertById: (serialNumber: string) => [{ serialNumber }, "cert"], - getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"] + getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"], + getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"] }; export const useGetCert = (serialNumber: string) => { @@ -38,3 +39,19 @@ export const useGetCertBody = (serialNumber: string) => { enabled: Boolean(serialNumber) }); }; + +export const useGetCertBundle = (serialNumber: string) => { + return useQuery({ + queryKey: certKeys.getCertBundle(serialNumber), + queryFn: async () => { + const { data } = await apiRequest.get<{ + certificate: string; + certificateChain: string; + serialNumber: string; + privateKey: string; + }>(`/api/v1/pki/certificates/${serialNumber}/bundle`); + return data; + }, + enabled: Boolean(serialNumber) + }); +}; diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index c11d7dc11..97acd6dfc 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -8,5 +8,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { [IdentityAuthMethod.AWS_AUTH]: "AWS Auth", [IdentityAuthMethod.AZURE_AUTH]: "Azure Auth", [IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth", + [IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth", [IdentityAuthMethod.JWT_AUTH]: "JWT Auth" }; diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index 415492e00..8a8d99fae 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -6,6 +6,7 @@ export enum IdentityAuthMethod { AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", OIDC_AUTH = "oidc-auth", + LDAP_AUTH = "ldap-auth", JWT_AUTH = "jwt-auth" } diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index f3b9fa012..bf49387ac 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -1,51 +1,4 @@ export { identityAuthToNameMap } from "./constants"; export { IdentityAuthMethod } from "./enums"; -export { - useAddIdentityAwsAuth, - useAddIdentityAzureAuth, - useAddIdentityGcpAuth, - useAddIdentityJwtAuth, - useAddIdentityKubernetesAuth, - useAddIdentityOidcAuth, - useAddIdentityTokenAuth, - useAddIdentityUniversalAuth, - useCreateIdentity, - useCreateIdentityUniversalAuthClientSecret, - useCreateTokenIdentityTokenAuth, - useDeleteIdentity, - useDeleteIdentityAwsAuth, - useDeleteIdentityAzureAuth, - useDeleteIdentityGcpAuth, - useDeleteIdentityJwtAuth, - useDeleteIdentityKubernetesAuth, - useDeleteIdentityOidcAuth, - useDeleteIdentityTokenAuth, - useDeleteIdentityUniversalAuth, - useRevokeIdentityTokenAuthToken, - useRevokeIdentityUniversalAuthClientSecret, - useUpdateIdentity, - useUpdateIdentityAwsAuth, - useUpdateIdentityAzureAuth, - useUpdateIdentityGcpAuth, - useUpdateIdentityJwtAuth, - useUpdateIdentityKubernetesAuth, - useUpdateIdentityOidcAuth, - useUpdateIdentityTokenAuth, - useUpdateIdentityTokenAuthToken, - useUpdateIdentityUniversalAuth -} from "./mutations"; -export { - useGetIdentityAwsAuth, - useGetIdentityAzureAuth, - useGetIdentityById, - useGetIdentityGcpAuth, - useGetIdentityJwtAuth, - useGetIdentityKubernetesAuth, - useGetIdentityOidcAuth, - useGetIdentityProjectMemberships, - useGetIdentityTokenAuth, - useGetIdentityTokensTokenAuth, - useGetIdentityUniversalAuth, - useGetIdentityUniversalAuthClientSecrets, - useSearchIdentities -} from "./queries"; +export * from "./mutations"; +export * from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index d68595ad5..e0077527f 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -10,6 +10,7 @@ import { AddIdentityGcpAuthDTO, AddIdentityJwtAuthDTO, AddIdentityKubernetesAuthDTO, + AddIdentityLdapAuthDTO, AddIdentityOidcAuthDTO, AddIdentityTokenAuthDTO, AddIdentityUniversalAuthDTO, @@ -25,6 +26,7 @@ import { DeleteIdentityGcpAuthDTO, DeleteIdentityJwtAuthDTO, DeleteIdentityKubernetesAuthDTO, + DeleteIdentityLdapAuthDTO, DeleteIdentityOidcAuthDTO, DeleteIdentityTokenAuthDTO, DeleteIdentityUniversalAuthClientSecretDTO, @@ -36,6 +38,7 @@ import { IdentityGcpAuth, IdentityJwtAuth, IdentityKubernetesAuth, + IdentityLdapAuth, IdentityOidcAuth, IdentityTokenAuth, IdentityUniversalAuth, @@ -47,6 +50,7 @@ import { UpdateIdentityGcpAuthDTO, UpdateIdentityJwtAuthDTO, UpdateIdentityKubernetesAuthDTO, + UpdateIdentityLdapAuthDTO, UpdateIdentityOidcAuthDTO, UpdateIdentityTokenAuthDTO, UpdateIdentityUniversalAuthDTO, @@ -1049,3 +1053,116 @@ export const useRevokeIdentityTokenAuthToken = () => { } }); }; + +export const useAddIdentityLdapAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + url, + bindDN, + bindPass, + searchBase, + searchFilter, + ldapCaCertificate, + allowedFields, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>( + `/api/v1/auth/ldap-auth/identities/${identityId}`, + { + url, + bindDN, + bindPass, + searchBase, + searchFilter, + ldapCaCertificate, + allowedFields, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + return data.identityLdapAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityLdapAuth(identityId) + }); + } + }); +}; + +export const useUpdateIdentityLdapAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + url, + bindDN, + bindPass, + searchBase, + searchFilter, + ldapCaCertificate, + allowedFields, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>( + `/api/v1/auth/ldap-auth/identities/${identityId}`, + { + url, + bindDN, + bindPass, + searchBase, + searchFilter, + ldapCaCertificate, + allowedFields, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + return data.identityLdapAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityLdapAuth(identityId) + }); + } + }); +}; + +export const useDeleteIdentityLdapAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { data } = await apiRequest.delete(`/api/v1/auth/ldap-auth/identities/${identityId}`); + return data.identityLdapAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityLdapAuth(identityId) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 6b8a1d1cc..3bc94534c 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -11,6 +11,7 @@ import { IdentityGcpAuth, IdentityJwtAuth, IdentityKubernetesAuth, + IdentityLdapAuth, IdentityMembership, IdentityMembershipOrg, IdentityOidcAuth, @@ -34,6 +35,7 @@ export const identitiesKeys = { getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const, getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const, getIdentityJwtAuth: (identityId: string) => [{ identityId }, "identity-jwt-auth"] as const, + getIdentityLdapAuth: (identityId: string) => [{ identityId }, "identity-ldap-auth"] as const, getIdentityTokensTokenAuth: (identityId: string) => [{ identityId }, "identity-tokens-token-auth"] as const, getIdentityProjectMemberships: (identityId: string) => @@ -231,6 +233,27 @@ export const useGetIdentityTokenAuth = ( }); }; +export const useGetIdentityLdapAuth = ( + identityId: string, + options?: TReactQueryOptions["options"] +) => { + return useQuery({ + queryKey: identitiesKeys.getIdentityLdapAuth(identityId), + queryFn: async () => { + const { + data: { identityLdapAuth } + } = await apiRequest.get<{ identityLdapAuth: IdentityLdapAuth }>( + `/api/v1/auth/ldap-auth/identities/${identityId}` + ); + return identityLdapAuth; + }, + staleTime: 0, + gcTime: 0, + ...options, + enabled: Boolean(identityId) && (options?.enabled ?? true) + }); +}; + export const useGetIdentityTokensTokenAuth = (identityId: string) => { return useQuery({ enabled: Boolean(identityId), diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index ca06219aa..c5f8cbc4a 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -425,6 +425,72 @@ export type IdentityTokenAuth = { accessTokenTrustedIps: IdentityTrustedIp[]; }; +export type AddIdentityLdapAuthDTO = { + organizationId: string; + identityId: string; + url: string; + bindDN: string; + bindPass: string; + searchBase: string; + searchFilter: string; + ldapCaCertificate?: string; + allowedFields?: { + key: string; + value: string; + }[]; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityLdapAuthDTO = { + identityId: string; + organizationId: string; + url?: string; + bindDN?: string; + bindPass?: string; + searchBase?: string; + searchFilter?: string; + ldapCaCertificate?: string; + allowedFields?: { + key: string; + value: string; + }[]; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + +export type DeleteIdentityLdapAuthDTO = { + organizationId: string; + identityId: string; +}; + +export type IdentityLdapAuth = { + url: string; + bindDN: string; + bindPass: string; + searchBase: string; + searchFilter: string; + ldapCaCertificate?: string; + allowedFields?: { + key: string; + value: string; + }[]; + + identityId: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + export type AddIdentityTokenAuthDTO = { organizationId: string; identityId: string; diff --git a/frontend/src/hooks/api/projectTemplates/queries.tsx b/frontend/src/hooks/api/projectTemplates/queries.tsx index f5863915a..89bc96715 100644 --- a/frontend/src/hooks/api/projectTemplates/queries.tsx +++ b/frontend/src/hooks/api/projectTemplates/queries.tsx @@ -6,14 +6,17 @@ import { TProjectTemplate, TProjectTemplateResponse } from "@app/hooks/api/projectTemplates/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; export const projectTemplateKeys = { all: ["project-template"] as const, - list: () => [...projectTemplateKeys.all, "list"] as const, + list: (projectType?: ProjectType) => + [...projectTemplateKeys.all, "list", ...(projectType ? [projectType] : [])] as const, byId: (templateId: string) => [...projectTemplateKeys.all, templateId] as const }; export const useListProjectTemplates = ( + type?: ProjectType, options?: Omit< UseQueryOptions< TProjectTemplate[], @@ -25,9 +28,11 @@ export const useListProjectTemplates = ( > ) => { return useQuery({ - queryKey: projectTemplateKeys.list(), + queryKey: projectTemplateKeys.list(type), queryFn: async () => { - const { data } = await apiRequest.get("/api/v1/project-templates"); + const { data } = await apiRequest.get("/api/v1/project-templates", { + params: { type } + }); return data.projectTemplates; }, diff --git a/frontend/src/hooks/api/projectTemplates/types.ts b/frontend/src/hooks/api/projectTemplates/types.ts index 37c6cc902..9f6ea5ef3 100644 --- a/frontend/src/hooks/api/projectTemplates/types.ts +++ b/frontend/src/hooks/api/projectTemplates/types.ts @@ -1,11 +1,13 @@ import { TProjectRole } from "@app/hooks/api/roles/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; export type TProjectTemplate = { id: string; name: string; + type: ProjectType; description?: string; roles: Pick[]; - environments: { name: string; slug: string; position: number }[]; + environments?: { name: string; slug: string; position: number }[] | null; createdAt: string; updatedAt: string; }; @@ -14,6 +16,7 @@ export type TListProjectTemplates = { projectTemplates: TProjectTemplate[] }; export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate }; export type TCreateProjectTemplateDTO = { + type: ProjectType; name: string; description?: string; }; diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts index 0a48c9f97..ee95e8c23 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -3,7 +3,9 @@ export enum ProjectMembershipRole { Member = "member", Custom = "custom", Viewer = "viewer", - NoAccess = "no-access" + NoAccess = "no-access", + SshHostBootstrapper = "ssh-host-bootstrapper", + KmsCryptographicOperator = "cryptographic-operator" } export type TGetProjectRolesDTO = { diff --git a/frontend/src/hooks/api/secretScanning/mutation.ts b/frontend/src/hooks/api/secretScanning/mutation.ts index 7298b9af1..5055da4aa 100644 --- a/frontend/src/hooks/api/secretScanning/mutation.ts +++ b/frontend/src/hooks/api/secretScanning/mutation.ts @@ -10,15 +10,17 @@ import { } from "./types"; export const useCreateNewInstallationSession = () => { - return useMutation<{ sessionId: string }, object, { organizationId: string }>({ - mutationFn: async (opt) => { - const { data } = await apiRequest.post( - "/api/v1/secret-scanning/create-installation-session/organization", - opt - ); - return data; + return useMutation<{ sessionId: string; gitAppSlug: string }, object, { organizationId: string }>( + { + mutationFn: async (opt) => { + const { data } = await apiRequest.post( + "/api/v1/secret-scanning/create-installation-session/organization", + opt + ); + return data; + } } - }); + ); }; export const useUpdateRiskStatus = () => { diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index eb4cdbdca..9e5eff713 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -1,4 +1,5 @@ export { useDebounce } from "./useDebounce"; +export * from "./useGetProjectTypeFromRoute"; export { usePagination } from "./usePagination"; export { usePersistentState } from "./usePersistentState"; export { usePopUp } from "./usePopUp"; diff --git a/frontend/src/hooks/useGetProjectTypeFromRoute.tsx b/frontend/src/hooks/useGetProjectTypeFromRoute.tsx new file mode 100644 index 000000000..b3b8f3d5e --- /dev/null +++ b/frontend/src/hooks/useGetProjectTypeFromRoute.tsx @@ -0,0 +1,22 @@ +import { useMemo } from "react"; +import { useRouterState } from "@tanstack/react-router"; + +import { ProjectType } from "@app/hooks/api/workspace/types"; + +export const useGetProjectTypeFromRoute = () => { + const { location } = useRouterState(); + + return useMemo(() => { + const segments = location.pathname.split("/"); + + let type: ProjectType | undefined; + + // location of project type can vary in router path, so we need to check all possible values + segments.forEach((segment) => { + if (Object.values(ProjectType).includes(segment as ProjectType)) + type = segment as ProjectType; + }); + + return type; + }, [location]); +}; diff --git a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx index 7cfc21838..6a2382191 100644 --- a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx +++ b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx @@ -11,11 +11,12 @@ import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2"; import { OrgPermissionSubjects, useOrgPermission, useServerConfig } from "@app/context"; import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types"; import { usePopUp } from "@app/hooks"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner"; import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar"; import { SidebarHeader } from "./components/SidebarHeader"; -import { DefaultSideBar, SecretSharingSideBar } from "./ProductsSideBar"; +import { DefaultSideBar, ProjectOverviewSideBar, SecretSharingSideBar } from "./ProductsSideBar"; export const OrganizationLayout = () => { const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); @@ -45,21 +46,38 @@ export const OrganizationLayout = () => { ] as string[] ).includes(location.pathname); + const isProjectOverviewOrSettingsPage = ( + [ + linkOptions({ to: "/organization/secret-manager/overview" }).to, + linkOptions({ to: "/organization/secret-manager/settings" }).to, + linkOptions({ to: "/organization/cert-manager/overview" }).to, + linkOptions({ to: "/organization/cert-manager/settings" }).to, + linkOptions({ to: "/organization/kms/overview" }).to, + linkOptions({ to: "/organization/kms/settings" }).to, + linkOptions({ to: "/organization/ssh/overview" }).to, + linkOptions({ to: "/organization/ssh/settings" }).to + ] as string[] + ).includes(location.pathname); + const shouldShowOrgSidebar = location.pathname.startsWith("/organization") && (!isSecretSharingPage || shouldShowProductsSidebar) && - !( - [ - linkOptions({ to: "/organization/secret-manager/overview" }).to, - linkOptions({ to: "/organization/cert-manager/overview" }).to, - linkOptions({ to: "/organization/ssh/overview" }).to, - linkOptions({ to: "/organization/kms/overview" }).to, - linkOptions({ to: "/organization/secret-scanning" }).to - ] as string[] - ).includes(location.pathname); + !([linkOptions({ to: "/organization/secret-scanning" }).to] as string[]).includes( + location.pathname + ); const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen"; + let SideBarComponent = ; + + if (isSecretSharingPage) { + SideBarComponent = ; + } else if (isProjectOverviewOrSettingsPage) { + SideBarComponent = ( + + ); + } + return ( <> @@ -80,10 +98,12 @@ export const OrganizationLayout = () => { className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900" > )} diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/ProjectOverviewSideBar.tsx b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/ProjectOverviewSideBar.tsx new file mode 100644 index 000000000..4b46cdd0a --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/ProjectOverviewSideBar.tsx @@ -0,0 +1,94 @@ +import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Link, useMatchRoute } from "@tanstack/react-router"; + +import { Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { ProjectType } from "@app/hooks/api/workspace/types"; + +type TProjectOverviewSideBarProps = { + type: ProjectType; +}; + +export const ProjectOverviewSideBar = ({ type }: TProjectOverviewSideBarProps) => { + const matchRoute = useMatchRoute(); + + const isOverviewActive = !!matchRoute({ + to: `/organization/${type}/overview`, + fuzzy: false + }); + + let label: string; + let icon: string; + let link: string; + + switch (type) { + case ProjectType.CertificateManager: + label = "Cert Management"; + icon = "note"; + link = "https://infisical.com/docs/documentation/platform/pki/overview"; + break; + case ProjectType.SecretManager: + label = "Secret Management"; + icon = "sliding-carousel"; + link = "https://infisical.com/docs/documentation/getting-started/introduction"; + break; + case ProjectType.KMS: + label = "KMS"; + icon = "unlock"; + link = "https://infisical.com/docs/documentation/platform/kms/overview"; + break; + case ProjectType.SSH: + label = "SSH"; + icon = "verified"; + link = "https://infisical.com/docs/documentation/platform/ssh/overview"; + break; + default: + throw new Error("Unknown project type"); + } + + return ( + <> + + + + + + {label} + + + + + + {({ isActive }) => ( + + Settings + + )} + + + + + ); +}; diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts index b2c79d873..057f5fe6b 100644 --- a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts +++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts @@ -1,2 +1,3 @@ export * from "./DefaultSideBar"; +export * from "./ProjectOverviewSideBar"; export * from "./SecretSharingSideBar"; diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index ba0b4f194..8cf807d56 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -269,7 +269,9 @@ export const MinimizedOrgSidebar = () => { @@ -282,7 +284,9 @@ export const MinimizedOrgSidebar = () => { @@ -294,7 +298,8 @@ export const MinimizedOrgSidebar = () => { {({ isActive }) => ( @@ -306,7 +311,8 @@ export const MinimizedOrgSidebar = () => { {({ isActive }) => ( diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx index af93b3c2a..790adff50 100644 --- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx @@ -31,6 +31,7 @@ import { import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel"; import { AuthPanel } from "./components/AuthPanel"; +import { CachingPanel } from "./components/CachingPanel"; import { EncryptionPanel } from "./components/EncryptionPanel"; import { IntegrationPanel } from "./components/IntegrationPanel"; import { UserPanel } from "./components/UserPanel"; @@ -42,7 +43,8 @@ enum TabSections { Integrations = "integrations", Users = "users", Identities = "identities", - Kmip = "kmip" + Kmip = "kmip", + Caching = "caching" } enum SignUpModes { @@ -164,6 +166,7 @@ export const OverviewPage = () => { Integrations User Identities Machine Identities + Caching
    @@ -408,6 +411,9 @@ export const OverviewPage = () => { + + +
    )} diff --git a/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx new file mode 100644 index 000000000..170a85c0d --- /dev/null +++ b/frontend/src/pages/admin/OverviewPage/components/CachingPanel.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from "react"; +import { faRotate } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Badge, Button, DeleteActionModal } from "@app/components/v2"; +import { useUser } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useInvalidateCache } from "@app/hooks/api"; +import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries"; +import { CacheType } from "@app/hooks/api/admin/types"; + +export const CachingPanel = () => { + const { mutateAsync: invalidateCache } = useInvalidateCache(); + const { user } = useUser(); + + const [type, setType] = useState(null); + const [shouldPoll, setShouldPoll] = useState(false); + + const { + data: invalidationStatus, + isFetching, + refetch + } = useGetInvalidatingCacheStatus(shouldPoll); + const isInvalidating = Boolean(shouldPoll && (isFetching || invalidationStatus)); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "invalidateCache" + ] as const); + + const handleInvalidateCacheSubmit = async () => { + if (!type || isInvalidating) return; + + try { + await invalidateCache({ type }); + createNotification({ text: `Began invalidating ${type} cache`, type: "success" }); + setShouldPoll(true); + handlePopUpClose("invalidateCache"); + } catch (err) { + console.error(err); + createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" }); + } + }; + + useEffect(() => { + if (isInvalidating) return; + + if (shouldPoll) { + setShouldPoll(false); + createNotification({ text: "Successfully invalidated cache", type: "success" }); + } + }, [isInvalidating, shouldPoll]); + + useEffect(() => { + refetch().then((v) => setShouldPoll(v.data || false)); + }, []); + + return ( + <> +
    +
    +
    + Secrets Cache + {isInvalidating && ( + + + Invalidating Cache + + )} +
    + + The encrypted secrets cache encompasses all secrets stored within the system and + provides a temporary, secure storage location for frequently accessed credentials. + +
    + + +
    + handlePopUpToggle("invalidateCache", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleInvalidateCacheSubmit} + /> + + ); +}; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx b/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx index c985f313f..4a4a22093 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/CertificatesPage.tsx @@ -3,7 +3,12 @@ import { useTranslation } from "react-i18next"; import { ProjectPermissionCan } from "@app/components/permissions"; import { PageHeader } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionCertificateActions, + ProjectPermissionSub, + useProjectPermission +} from "@app/context"; import { PkiCollectionSection } from "../AlertingPage/components"; import { CertificatesSection } from "./components"; @@ -17,7 +22,7 @@ export const CertificatesPage = () => { ProjectPermissionSub.PkiCollections ); const canAccessCerts = permission.can( - ProjectPermissionActions.Read, + ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates ); @@ -40,7 +45,7 @@ export const CertificatesPage = () => { )} diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx index 01c79589c..54620f1d6 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx @@ -1,5 +1,11 @@ import { Modal, ModalContent } from "@app/components/v2"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionSub, + useProjectPermission +} from "@app/context"; import { useGetCertBody } from "@app/hooks/api"; +import { useGetCertBundle } from "@app/hooks/api/certificates/queries"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertificateContent } from "./CertificateContent"; @@ -10,10 +16,29 @@ type Props = { }; export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => { - const { data } = useGetCertBody( - (popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || "" + const { permission } = useProjectPermission(); + + const serialNumber = + (popUp?.certificateCert?.data as { serialNumber: string })?.serialNumber || ""; + + const canReadPrivateKey = permission.can( + ProjectPermissionCertificateActions.ReadPrivateKey, + ProjectPermissionSub.Certificates ); + // useGetCertBundle fails unless user has the correct permissions + const { data: bundleData } = useGetCertBundle(serialNumber); + const { data: bodyData } = useGetCertBody(serialNumber); + + const data: + | { + certificate: string; + certificateChain: string; + serialNumber: string; + privateKey?: string; + } + | undefined = canReadPrivateKey ? bundleData : bodyData; + return ( { serialNumber={data.serialNumber} certificate={data.certificate} certificateChain={data.certificateChain} + privateKey={data.privateKey} /> ) : (
    diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx index d5f94e7b7..ef8b09dac 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx @@ -4,7 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; import { useDeleteCert } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -50,7 +54,7 @@ export const CertificatesSection = () => {

    Certificates

    {(isAllowed) => ( diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx index dcc832bfb..8cf7dda24 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx @@ -30,7 +30,11 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; import { useListWorkspaceCertificates } from "@app/hooks/api"; import { CertStatus } from "@app/hooks/api/certificates/enums"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -110,7 +114,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { {(isAllowed) => ( @@ -131,7 +135,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} {(isAllowed) => ( @@ -152,7 +156,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} {(isAllowed) => ( @@ -173,7 +177,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} {(isAllowed) => ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx index 1380ebb39..0444b3bd1 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx @@ -13,6 +13,7 @@ import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm"; import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm"; import { IdentityJwtAuthForm } from "./IdentityJwtAuthForm"; import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm"; +import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm"; import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm"; import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; @@ -46,6 +47,7 @@ const identityAuthMethods = [ { label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }, { label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH }, { label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }, + { label: "LDAP Auth", value: IdentityAuthMethod.LDAP_AUTH }, { label: "JWT Auth", value: IdentityAuthMethod.JWT_AUTH @@ -186,6 +188,16 @@ export const IdentityAuthMethodModalContent = ({ handlePopUpToggle={handlePopUpToggle} /> ) + }, + + [IdentityAuthMethod.LDAP_AUTH]: { + render: () => ( + + ) } }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx new file mode 100644 index 000000000..62080bfd1 --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx @@ -0,0 +1,608 @@ +import { useEffect, useState } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faQuestionCircle, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + IconButton, + Input, + Tab, + TabList, + TabPanel, + Tabs, + TextArea, + Tooltip +} from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { + useAddIdentityLdapAuth, + useGetIdentityLdapAuth, + useUpdateIdentityLdapAuth +} from "@app/hooks/api"; +import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { IdentityFormTab } from "./types"; + +const schema = z + .object({ + url: z.string().min(1), + bindDN: z.string(), + bindPass: z.string(), + searchBase: z.string(), + searchFilter: z.string(), // defaults to (uid={{username}}) + ldapCaCertificate: z + .string() + .optional() + .transform((val) => val || undefined), + allowedFields: z + .object({ + key: z.string().trim(), + value: z + .string() + .trim() + .transform((val) => val.replace(/\s/g, "")) + }) + .array() + .optional(), + + accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token TTL cannot be greater than 315360000" + }), + accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, { + message: "Access Token Max TTL cannot be greater than 315360000" + }), + accessTokenNumUsesLimit: z.string(), + accessTokenTrustedIps: z + .array( + z.object({ + ipAddress: z.string().max(50) + }) + ) + .min(1) + }) + .required(); + +export type FormData = z.infer; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + state?: boolean + ) => void; + identityId?: string; + isUpdate?: boolean; +}; + +export const IdentityLdapAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityId, + isUpdate +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityLdapAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth(); + const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); + + const { data } = useGetIdentityLdapAuth(identityId ?? "", { + enabled: isUpdate + }); + + const { + control, + handleSubmit, + reset, + + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + url: "", + bindDN: "", + bindPass: "", + searchBase: "", + searchFilter: "(uid={{username}})", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + const { + fields: allowedFieldsFields, + append: appendAllowedField, + remove: removeAllowedField + } = useFieldArray({ control, name: "allowedFields" }); + + useEffect(() => { + if (data) { + reset({ + url: data.url, + bindDN: data.bindDN, + bindPass: data.bindPass, + searchBase: data.searchBase, + searchFilter: data.searchFilter, + ldapCaCertificate: data.ldapCaCertificate || undefined, + allowedFields: data.allowedFields, + accessTokenTTL: String(data.accessTokenTTL), + accessTokenMaxTTL: String(data.accessTokenMaxTTL), + accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), + accessTokenTrustedIps: data.accessTokenTrustedIps.map( + ({ ipAddress, prefix }: IdentityTrustedIp) => { + return { + ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` + }; + } + ) + }); + } else { + reset({ + url: "", + bindDN: "", + bindPass: "", + searchBase: "", + searchFilter: "(uid={{username}})", + ldapCaCertificate: undefined, + allowedFields: [], + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + useEffect(() => { + if (!subscription?.ldap) { + handlePopUpOpen("upgradePlan"); + handlePopUpToggle("identityAuthMethod", false); + } + }, [subscription]); + + const onFormSubmit = async ({ + url, + bindDN, + bindPass, + searchBase, + searchFilter, + ldapCaCertificate, + allowedFields, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityId) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + identityId, + url, + bindDN, + bindPass, + searchBase, + searchFilter, + ldapCaCertificate, + allowedFields, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId, + url, + bindDN, + bindPass, + searchBase, + searchFilter, + ldapCaCertificate, + allowedFields, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); + } catch { + createNotification({ + text: `Failed to ${isUpdate ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( + { + setTabValue( + [ + "url", + "bindDN", + "bindPass", + "searchBase", + "searchFilter", + "accessTokenTTL", + "allowedFields", + "accessTokenMaxTTL", + "accessTokenNumUsesLimit" + ].includes(Object.keys(fields)[0]) + ? IdentityFormTab.Configuration + : IdentityFormTab.Advanced + ); + })} + > + setTabValue(value as IdentityFormTab)}> + + Configuration + Advanced + + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + + ( + + + + )} + /> + + {allowedFieldsFields.map(({ id }, index) => ( +
    + { + const isFirstField = index === 0; + + return ( + +

    + Specify the fields that the user must contain in their LDAP entry + in order to authenticate with this identity. If nothing is + specified, all users in the configured LDAP directory will be able + to authenticate. +

    + You can specify multiple required attributes by separating them + with a comma. +

    +

    +
    +

    Example:

    +

    + 'uid' → 'user1,user2,user3' +
    + 'mail' → 'user@example.com' +

    +
    + +

    + The above example would allow users with the UID user1, user2, or + user3 to authenticate but only if their emails also match + user@example.com +

    +
    + } + > + + + ) : undefined + } + isError={Boolean(error)} + errorText={error?.message} + > + field.onChange(e)} + placeholder="uid" + /> + + ); + }} + /> + { + return ( + + field.onChange(e)} + placeholder="userid1,userid2,userid3" + /> + + ); + }} + /> + removeAllowedField(index)} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
    + ))} +
    + +
    + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + + + ( + +