diff --git a/README.md b/README.md index e5b493107..9a825e09d 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus ### Key Management (KMS): -- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API. +- **[Cryptographic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API. - **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data. ### General Platform: diff --git a/backend/package-lock.json b/backend/package-lock.json index 2fba00120..9d0a3c8c9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -49,7 +49,6 @@ "@sindresorhus/slugify": "1.1.0", "@slack/oauth": "^3.0.1", "@slack/web-api": "^7.3.4", - "@team-plain/typescript-sdk": "^4.6.1", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", @@ -5678,14 +5677,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/@grpc/grpc-js": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", @@ -9970,18 +9961,6 @@ "optional": true, "peer": true }, - "node_modules/@team-plain/typescript-sdk": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-4.6.1.tgz", - "integrity": "sha512-Uy9QJXu9U7bJb6WXL9sArGk7FXPpzdqBd6q8tAF1vexTm8fbTJRqcikTKxGtZmNADt+C2SapH3cApM4oHpO4lQ==", - "dependencies": { - "@graphql-typed-document-node/core": "^3.2.0", - "ajv": "^8.12.0", - "ajv-formats": "^2.1.1", - "graphql": "^16.6.0", - "zod": "3.22.4" - } - }, "node_modules/@techteamer/ocsp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz", @@ -15180,14 +15159,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/graphql": { - "version": "16.9.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", - "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, "node_modules/gtoken": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index 0dafc475c..a7321f67d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -157,7 +157,6 @@ "@sindresorhus/slugify": "1.1.0", "@slack/oauth": "^3.0.1", "@slack/web-api": "^7.3.4", - "@team-plain/typescript-sdk": "^4.6.1", "@ucast/mongo2js": "^1.3.4", "ajv": "^8.12.0", "argon2": "^0.31.2", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 2c6e13272..7ae0b64fd 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -54,6 +54,7 @@ import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-acces import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service"; 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 { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; @@ -164,6 +165,7 @@ declare module "fastify" { identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory; identityOidcAuth: TIdentityOidcAuthServiceFactory; + identityJwtAuth: TIdentityJwtAuthServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; accessApprovalRequest: TAccessApprovalRequestServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 0fa1f3c2e..6f7e36c7d 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -98,6 +98,9 @@ import { TIdentityGcpAuths, TIdentityGcpAuthsInsert, TIdentityGcpAuthsUpdate, + TIdentityJwtAuths, + TIdentityJwtAuthsInsert, + TIdentityJwtAuthsUpdate, TIdentityKubernetesAuths, TIdentityKubernetesAuthsInsert, TIdentityKubernetesAuthsUpdate, @@ -199,6 +202,9 @@ import { TProjectSlackConfigs, TProjectSlackConfigsInsert, TProjectSlackConfigsUpdate, + TProjectSplitBackfillIds, + TProjectSplitBackfillIdsInsert, + TProjectSplitBackfillIdsUpdate, TProjectsUpdate, TProjectTemplates, TProjectTemplatesInsert, @@ -630,6 +636,11 @@ declare module "knex/types/tables" { TIdentityOidcAuthsInsert, TIdentityOidcAuthsUpdate >; + [TableName.IdentityJwtAuth]: KnexOriginal.CompositeTableType< + TIdentityJwtAuths, + TIdentityJwtAuthsInsert, + TIdentityJwtAuthsUpdate + >; [TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType< TIdentityUaClientSecrets, TIdentityUaClientSecretsInsert, @@ -870,5 +881,10 @@ declare module "knex/types/tables" { TProjectTemplatesUpdate >; [TableName.TotpConfig]: KnexOriginal.CompositeTableType; + [TableName.ProjectSplitBackfillIds]: KnexOriginal.CompositeTableType< + TProjectSplitBackfillIds, + TProjectSplitBackfillIdsInsert, + TProjectSplitBackfillIdsUpdate + >; } } diff --git a/backend/src/db/migrations/20241209144123_add-identity-jwt-auth.ts b/backend/src/db/migrations/20241209144123_add-identity-jwt-auth.ts new file mode 100644 index 000000000..03594b77c --- /dev/null +++ b/backend/src/db/migrations/20241209144123_add-identity-jwt-auth.ts @@ -0,0 +1,34 @@ +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.IdentityJwtAuth))) { + await knex.schema.createTable(TableName.IdentityJwtAuth, (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.string("configurationType").notNullable(); + t.string("jwksUrl").notNullable(); + t.binary("encryptedJwksCaCert").notNullable(); + t.binary("encryptedPublicKeys").notNullable(); + t.string("boundIssuer").notNullable(); + t.string("boundAudiences").notNullable(); + t.jsonb("boundClaims").notNullable(); + t.string("boundSubject").notNullable(); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.IdentityJwtAuth); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityJwtAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityJwtAuth); +} diff --git a/backend/src/db/migrations/20241213122320_add-index-for-secret-version-v2-folder.ts b/backend/src/db/migrations/20241213122320_add-index-for-secret-version-v2-folder.ts new file mode 100644 index 000000000..96e8f08f6 --- /dev/null +++ b/backend/src/db/migrations/20241213122320_add-index-for-secret-version-v2-folder.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SecretVersionV2, "folderId")) { + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + t.index("folderId"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SecretVersionV2, "folderId")) { + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + t.dropIndex("folderId"); + }); + } +} diff --git a/backend/src/db/migrations/20241213122350_project-split-to-products.ts b/backend/src/db/migrations/20241213122350_project-split-to-products.ts new file mode 100644 index 000000000..d7a00a801 --- /dev/null +++ b/backend/src/db/migrations/20241213122350_project-split-to-products.ts @@ -0,0 +1,297 @@ +import slugify from "@sindresorhus/slugify"; +import { Knex } from "knex"; +import { v4 as uuidV4 } from "uuid"; + +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { ProjectType, TableName } from "../schemas"; + +/* eslint-disable no-await-in-loop,@typescript-eslint/ban-ts-comment */ +const newProject = async (knex: Knex, projectId: string, projectType: ProjectType) => { + const newProjectId = uuidV4(); + const project = await knex(TableName.Project).where("id", projectId).first(); + await knex(TableName.Project).insert({ + ...project, + type: projectType, + // @ts-ignore id is required + id: newProjectId, + slug: slugify(`${project?.name}-${alphaNumericNanoId(4)}`) + }); + + const customRoleMapping: Record = {}; + const projectCustomRoles = await knex(TableName.ProjectRoles).where("projectId", projectId); + if (projectCustomRoles.length) { + await knex.batchInsert( + TableName.ProjectRoles, + projectCustomRoles.map((el) => { + const id = uuidV4(); + customRoleMapping[el.id] = id; + return { + ...el, + id, + projectId: newProjectId, + permissions: el.permissions ? JSON.stringify(el.permissions) : el.permissions + }; + }) + ); + } + const groupMembershipMapping: Record = {}; + const groupMemberships = await knex(TableName.GroupProjectMembership).where("projectId", projectId); + if (groupMemberships.length) { + await knex.batchInsert( + TableName.GroupProjectMembership, + groupMemberships.map((el) => { + const id = uuidV4(); + groupMembershipMapping[el.id] = id; + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + const groupMembershipRoles = await knex(TableName.GroupProjectMembershipRole).whereIn( + "projectMembershipId", + groupMemberships.map((el) => el.id) + ); + if (groupMembershipRoles.length) { + await knex.batchInsert( + TableName.GroupProjectMembershipRole, + groupMembershipRoles.map((el) => { + const id = uuidV4(); + const projectMembershipId = groupMembershipMapping[el.projectMembershipId]; + const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return { ...el, id, projectMembershipId, customRoleId }; + }) + ); + } + + const identityProjectMembershipMapping: Record = {}; + const identities = await knex(TableName.IdentityProjectMembership).where("projectId", projectId); + if (identities.length) { + await knex.batchInsert( + TableName.IdentityProjectMembership, + identities.map((el) => { + const id = uuidV4(); + identityProjectMembershipMapping[el.id] = id; + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + const identitiesRoles = await knex(TableName.IdentityProjectMembershipRole).whereIn( + "projectMembershipId", + identities.map((el) => el.id) + ); + if (identitiesRoles.length) { + await knex.batchInsert( + TableName.IdentityProjectMembershipRole, + identitiesRoles.map((el) => { + const id = uuidV4(); + const projectMembershipId = identityProjectMembershipMapping[el.projectMembershipId]; + const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return { ...el, id, projectMembershipId, customRoleId }; + }) + ); + } + + const projectMembershipMapping: Record = {}; + const projectUserMembers = await knex(TableName.ProjectMembership).where("projectId", projectId); + if (projectUserMembers.length) { + await knex.batchInsert( + TableName.ProjectMembership, + projectUserMembers.map((el) => { + const id = uuidV4(); + projectMembershipMapping[el.id] = id; + return { ...el, id, projectId: newProjectId }; + }) + ); + } + const membershipRoles = await knex(TableName.ProjectUserMembershipRole).whereIn( + "projectMembershipId", + projectUserMembers.map((el) => el.id) + ); + if (membershipRoles.length) { + await knex.batchInsert( + TableName.ProjectUserMembershipRole, + membershipRoles.map((el) => { + const id = uuidV4(); + const projectMembershipId = projectMembershipMapping[el.projectMembershipId]; + const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return { ...el, id, projectMembershipId, customRoleId }; + }) + ); + } + + const kmsKeys = await knex(TableName.KmsKey).where("projectId", projectId).andWhere("isReserved", true); + if (kmsKeys.length) { + await knex.batchInsert( + TableName.KmsKey, + kmsKeys.map((el) => { + const id = uuidV4(); + const slug = slugify(alphaNumericNanoId(8).toLowerCase()); + return { ...el, id, slug, projectId: newProjectId }; + }) + ); + } + + const projectBot = await knex(TableName.ProjectBot).where("projectId", projectId).first(); + if (projectBot) { + const newProjectBot = { ...projectBot, id: uuidV4(), projectId: newProjectId }; + await knex(TableName.ProjectBot).insert(newProjectBot); + } + + const projectKeys = await knex(TableName.ProjectKeys).where("projectId", projectId); + if (projectKeys.length) { + await knex.batchInsert( + TableName.ProjectKeys, + projectKeys.map((el) => { + const id = uuidV4(); + return { ...el, id, projectId: newProjectId }; + }) + ); + } + + return newProjectId; +}; + +const BATCH_SIZE = 500; +export async function up(knex: Knex): Promise { + const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds); + if (!hasSplitMappingTable) { + await knex.schema.createTable(TableName.ProjectSplitBackfillIds, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("sourceProjectId", 36).notNullable(); + t.foreign("sourceProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.string("destinationProjectType").notNullable(); + t.string("destinationProjectId", 36).notNullable(); + t.foreign("destinationProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + }); + } + + const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type"); + if (!hasTypeColumn) { + await knex.schema.alterTable(TableName.Project, (t) => { + t.string("type"); + }); + + let projectsToBeTyped; + do { + // eslint-disable-next-line no-await-in-loop + projectsToBeTyped = await knex(TableName.Project).whereNull("type").limit(BATCH_SIZE).select("id"); + if (projectsToBeTyped.length) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.Project) + .whereIn( + "id", + projectsToBeTyped.map((el) => el.id) + ) + .update({ type: ProjectType.SecretManager }); + } + } while (projectsToBeTyped.length > 0); + + const projectsWithCertificates = await knex(TableName.CertificateAuthority) + .distinct("projectId") + .select("projectId"); + /* eslint-disable no-await-in-loop,no-param-reassign */ + for (const { projectId } of projectsWithCertificates) { + const newProjectId = await newProject(knex, projectId, ProjectType.CertificateManager); + await knex(TableName.CertificateAuthority).where("projectId", projectId).update({ projectId: newProjectId }); + await knex(TableName.PkiAlert).where("projectId", projectId).update({ projectId: newProjectId }); + await knex(TableName.PkiCollection).where("projectId", projectId).update({ projectId: newProjectId }); + await knex(TableName.ProjectSplitBackfillIds).insert({ + sourceProjectId: projectId, + destinationProjectType: ProjectType.CertificateManager, + destinationProjectId: newProjectId + }); + } + + const projectsWithCmek = await knex(TableName.KmsKey) + .where("isReserved", false) + .whereNotNull("projectId") + .distinct("projectId") + .select("projectId"); + for (const { projectId } of projectsWithCmek) { + if (projectId) { + const newProjectId = await newProject(knex, projectId, ProjectType.KMS); + await knex(TableName.KmsKey) + .where({ + isReserved: false, + projectId + }) + .update({ projectId: newProjectId }); + await knex(TableName.ProjectSplitBackfillIds).insert({ + sourceProjectId: projectId, + destinationProjectType: ProjectType.KMS, + destinationProjectId: newProjectId + }); + } + } + + /* eslint-enable */ + await knex.schema.alterTable(TableName.Project, (t) => { + t.string("type").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type"); + const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds); + + if (hasTypeColumn && hasSplitMappingTable) { + const splitProjectMappings = await knex(TableName.ProjectSplitBackfillIds).where({}); + const certMapping = splitProjectMappings.filter( + (el) => el.destinationProjectType === ProjectType.CertificateManager + ); + /* eslint-disable no-await-in-loop */ + for (const project of certMapping) { + await knex(TableName.CertificateAuthority) + .where("projectId", project.destinationProjectId) + .update({ projectId: project.sourceProjectId }); + await knex(TableName.PkiAlert) + .where("projectId", project.destinationProjectId) + .update({ projectId: project.sourceProjectId }); + await knex(TableName.PkiCollection) + .where("projectId", project.destinationProjectId) + .update({ projectId: project.sourceProjectId }); + } + + /* eslint-enable */ + const kmsMapping = splitProjectMappings.filter((el) => el.destinationProjectType === ProjectType.KMS); + /* eslint-disable no-await-in-loop */ + for (const project of kmsMapping) { + await knex(TableName.KmsKey) + .where({ + isReserved: false, + projectId: project.destinationProjectId + }) + .update({ projectId: project.sourceProjectId }); + } + /* eslint-enable */ + await knex(TableName.ProjectMembership) + .whereIn( + "projectId", + splitProjectMappings.map((el) => el.destinationProjectId) + ) + .delete(); + await knex(TableName.ProjectRoles) + .whereIn( + "projectId", + splitProjectMappings.map((el) => el.destinationProjectId) + ) + .delete(); + await knex(TableName.Project) + .whereIn( + "id", + splitProjectMappings.map((el) => el.destinationProjectId) + ) + .delete(); + + await knex.schema.alterTable(TableName.Project, (t) => { + t.dropColumn("type"); + }); + } + + if (hasSplitMappingTable) { + await knex.schema.dropTableIfExists(TableName.ProjectSplitBackfillIds); + } +} diff --git a/backend/src/db/migrations/20241130015511_ssh-mgmt.ts b/backend/src/db/migrations/20241216013357_ssh-mgmt.ts similarity index 100% rename from backend/src/db/migrations/20241130015511_ssh-mgmt.ts rename to backend/src/db/migrations/20241216013357_ssh-mgmt.ts diff --git a/backend/src/db/schemas/identity-jwt-auths.ts b/backend/src/db/schemas/identity-jwt-auths.ts new file mode 100644 index 000000000..1d3ea9c03 --- /dev/null +++ b/backend/src/db/schemas/identity-jwt-auths.ts @@ -0,0 +1,33 @@ +// 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 IdentityJwtAuthsSchema = 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(), + configurationType: z.string(), + jwksUrl: z.string(), + encryptedJwksCaCert: zodBuffer, + encryptedPublicKeys: zodBuffer, + boundIssuer: z.string(), + boundAudiences: z.string(), + boundClaims: z.unknown(), + boundSubject: z.string(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TIdentityJwtAuths = z.infer; +export type TIdentityJwtAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityJwtAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index c5dcecfd8..19f45eb33 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -30,6 +30,7 @@ export * from "./identity-access-tokens"; export * from "./identity-aws-auths"; export * from "./identity-azure-auths"; export * from "./identity-gcp-auths"; +export * from "./identity-jwt-auths"; export * from "./identity-kubernetes-auths"; export * from "./identity-metadata"; export * from "./identity-oidc-auths"; @@ -64,6 +65,7 @@ export * from "./project-keys"; export * from "./project-memberships"; export * from "./project-roles"; export * from "./project-slack-configs"; +export * from "./project-split-backfill-ids"; export * from "./project-templates"; export * from "./project-user-additional-privilege"; export * from "./project-user-membership-roles"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 0f2c1ae49..3156c9742 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -73,6 +73,7 @@ export enum TableName { IdentityUaClientSecret = "identity_ua_client_secrets", IdentityAwsAuth = "identity_aws_auths", IdentityOidcAuth = "identity_oidc_auths", + IdentityJwtAuth = "identity_jwt_auths", IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", @@ -110,6 +111,7 @@ export enum TableName { SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2", SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2", SnapshotSecretV2 = "secret_snapshot_secrets_v2", + ProjectSplitBackfillIds = "project_split_backfill_ids", // junction tables with tags SecretV2JnTag = "secret_v2_tag_junction", JnSecretTag = "secret_tag_junction", @@ -201,5 +203,13 @@ export enum IdentityAuthMethod { GCP_AUTH = "gcp-auth", AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", - OIDC_AUTH = "oidc-auth" + OIDC_AUTH = "oidc-auth", + JWT_AUTH = "jwt-auth" +} + +export enum ProjectType { + SecretManager = "secret-manager", + CertificateManager = "cert-manager", + KMS = "kms", + SSH = "ssh" } diff --git a/backend/src/db/schemas/project-split-backfill-ids.ts b/backend/src/db/schemas/project-split-backfill-ids.ts new file mode 100644 index 000000000..182d85049 --- /dev/null +++ b/backend/src/db/schemas/project-split-backfill-ids.ts @@ -0,0 +1,21 @@ +// 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 { TImmutableDBKeys } from "./models"; + +export const ProjectSplitBackfillIdsSchema = z.object({ + id: z.string().uuid(), + sourceProjectId: z.string(), + destinationProjectType: z.string(), + destinationProjectId: z.string() +}); + +export type TProjectSplitBackfillIds = z.infer; +export type TProjectSplitBackfillIdsInsert = Omit, TImmutableDBKeys>; +export type TProjectSplitBackfillIdsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index 5c5f9774b..ec43be292 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -24,7 +24,8 @@ export const ProjectsSchema = z.object({ auditLogsRetentionDays: z.number().nullable().optional(), kmsSecretManagerKeyId: z.string().uuid().nullable().optional(), kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(), - description: z.string().nullable().optional() + description: z.string().nullable().optional(), + type: z.string() }); export type TProjects = z.infer; diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts index 934130494..b6c80bb63 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -4,7 +4,7 @@ import { Knex } from "knex"; import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; -import { ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; +import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ @@ -24,6 +24,7 @@ export async function seed(knex: Knex): Promise { name: seedData1.project.name, orgId: seedData1.organization.id, slug: "first-project", + type: ProjectType.SecretManager, // eslint-disable-next-line // @ts-ignore id: seedData1.project.id diff --git a/backend/src/db/seeds/4-project-v3.ts b/backend/src/db/seeds/4-project-v3.ts index 60431919d..f89b965a6 100644 --- a/backend/src/db/seeds/4-project-v3.ts +++ b/backend/src/db/seeds/4-project-v3.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { ProjectMembershipRole, ProjectVersion, TableName } from "../schemas"; +import { ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ @@ -16,6 +16,7 @@ export async function seed(knex: Knex): Promise { orgId: seedData1.organization.id, slug: seedData1.projectV3.slug, version: ProjectVersion.V3, + type: ProjectType.SecretManager, // eslint-disable-next-line // @ts-ignore id: seedData1.projectV3.id diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 24436e695..72de6810a 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -86,13 +87,15 @@ export const accessApprovalPolicyServiceFactory = ({ if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, project.id, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval @@ -190,14 +193,7 @@ export const accessApprovalPolicyServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); // Anyone in the project should be able to get the policies. - /* const { permission } = */ await permissionService.getProjectPermission( - actor, - actorId, - project.id, - actorAuthMethod, - actorOrgId - ); - // ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null }); return accessApprovalPolicies; @@ -241,13 +237,14 @@ export const accessApprovalPolicyServiceFactory = ({ if (!accessApprovalPolicy) { throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, accessApprovalPolicy.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); @@ -324,13 +321,14 @@ export const accessApprovalPolicyServiceFactory = ({ const policy = await accessApprovalPolicyDAL.findById(policyId); if (!policy) throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, policy.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval 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 adc3debaa..1338257e2 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -98,6 +98,11 @@ export enum EventType { 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", @@ -917,6 +922,67 @@ interface GetIdentityOidcAuthEvent { }; } +interface LoginIdentityJwtAuthEvent { + type: EventType.LOGIN_IDENTITY_JWT_AUTH; + metadata: { + identityId: string; + identityJwtAuthId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityJwtAuthEvent { + type: EventType.ADD_IDENTITY_JWT_AUTH; + metadata: { + identityId: string; + configurationType: string; + jwksUrl?: string; + jwksCaCert: string; + publicKeys: string[]; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface UpdateIdentityJwtAuthEvent { + type: EventType.UPDATE_IDENTITY_JWT_AUTH; + metadata: { + identityId: string; + configurationType?: string; + jwksUrl?: string; + jwksCaCert?: string; + publicKeys?: string[]; + boundIssuer?: string; + boundAudiences?: string; + boundClaims?: Record; + boundSubject?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface DeleteIdentityJwtAuthEvent { + type: EventType.REVOKE_IDENTITY_JWT_AUTH; + metadata: { + identityId: string; + }; +} + +interface GetIdentityJwtAuthEvent { + type: EventType.GET_IDENTITY_JWT_AUTH; + metadata: { + identityId: string; + }; +} + interface CreateEnvironmentEvent { type: EventType.CREATE_ENVIRONMENT; metadata: { @@ -1867,6 +1933,11 @@ export type Event = | DeleteIdentityOidcAuthEvent | UpdateIdentityOidcAuthEvent | GetIdentityOidcAuthEvent + | LoginIdentityJwtAuthEvent + | AddIdentityJwtAuthEvent + | UpdateIdentityJwtAuthEvent + | GetIdentityJwtAuthEvent + | DeleteIdentityJwtAuthEvent | CreateEnvironmentEvent | GetEnvironmentEvent | UpdateEnvironmentEvent diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 82d1604eb..81e76ff71 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import ms from "ms"; -import { SecretKeyEncoding } from "@app/db/schemas"; +import { ProjectType, SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { @@ -67,13 +67,14 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -146,13 +147,14 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -225,13 +227,14 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 5eff1cdcf..db60b3e57 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { SecretKeyEncoding } from "@app/db/schemas"; +import { ProjectType, SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { @@ -73,13 +73,14 @@ export const dynamicSecretServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.CreateRootCredential, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -144,13 +145,14 @@ export const dynamicSecretServiceFactory = ({ const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.EditRootCredential, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) @@ -227,13 +229,14 @@ export const dynamicSecretServiceFactory = ({ const projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.DeleteRootCredential, subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 730ad3bbc..3a3c82414 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -269,6 +269,7 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("orgId").withSchema(TableName.Project), + db.ref("type").withSchema(TableName.Project).as("projectType"), db.ref("id").withSchema(TableName.Project).as("projectId") ); @@ -284,13 +285,15 @@ export const permissionDALFactory = (db: TDbClient) => { membershipCreatedAt, groupMembershipCreatedAt, groupMembershipUpdatedAt, - membershipUpdatedAt + membershipUpdatedAt, + projectType }) => ({ orgId, orgAuthEnforced, userId, projectId, username, + projectType, id: membershipId || groupMembershipId, createdAt: membershipCreatedAt || groupMembershipCreatedAt, updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt @@ -449,6 +452,7 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project + db.ref("type").withSchema(TableName.Project).as("projectType"), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), @@ -480,7 +484,14 @@ export const permissionDALFactory = (db: TDbClient) => { const permission = sqlNestRelationships({ data: docs, key: "membershipId", - parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({ + parentMapper: ({ + membershipId, + membershipCreatedAt, + membershipUpdatedAt, + orgId, + identityName, + projectType + }) => ({ id: membershipId, identityId, username: identityName, @@ -488,6 +499,7 @@ export const permissionDALFactory = (db: TDbClient) => { createdAt: membershipCreatedAt, updatedAt: membershipUpdatedAt, orgId, + projectType, // just a prefilled value orgAuthEnforced: false }), diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 13645b8f1..96e189115 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -6,6 +6,7 @@ import handlebars from "handlebars"; import { OrgMembershipRole, ProjectMembershipRole, + ProjectType, ServiceTokenScopes, TIdentityProjectMemberships, TProjectMemberships @@ -255,6 +256,13 @@ export const permissionServiceFactory = ({ return { permission, membership: userProjectPermission, + ForbidOnInvalidProjectType: (productType: ProjectType) => { + if (productType !== userProjectPermission.projectType) { + throw new BadRequestError({ + message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${productType} are not allowed.` + }); + } + }, hasRole: (role: string) => userProjectPermission.roles.findIndex( ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug @@ -323,6 +331,13 @@ export const permissionServiceFactory = ({ return { permission, membership: identityProjectPermission, + ForbidOnInvalidProjectType: (productType: ProjectType) => { + if (productType !== identityProjectPermission.projectType) { + throw new BadRequestError({ + message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${productType} are not allowed.` + }); + } + }, hasRole: (role: string) => identityProjectPermission.roles.findIndex( ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug @@ -361,7 +376,14 @@ export const permissionServiceFactory = ({ const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); return { permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), - membership: undefined + membership: undefined, + ForbidOnInvalidProjectType: (productType: ProjectType) => { + if (productType !== serviceTokenProject.type) { + throw new BadRequestError({ + message: `The project is of type ${serviceTokenProject.type}. Operations of type ${productType} are not allowed.` + }); + } + } }; }; @@ -370,6 +392,7 @@ export const permissionServiceFactory = ({ permission: MongoAbility; membership: undefined; hasRole: (arg: string) => boolean; + ForbidOnInvalidProjectType: (type: ProjectType) => void; } // service token doesn't have both membership and roles : { permission: MongoAbility; @@ -379,6 +402,7 @@ export const permissionServiceFactory = ({ roles: Array<{ role: string }>; }; hasRole: (role: string) => boolean; + ForbidOnInvalidProjectType: (type: ProjectType) => void; }; const getProjectPermission = async ( diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 4e7bf6d15..b0de6ad75 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import picomatch from "picomatch"; +import { ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -78,13 +79,14 @@ export const secretApprovalPolicyServiceFactory = ({ if (!groupApprovers.length && approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval @@ -191,13 +193,14 @@ export const secretApprovalPolicyServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, secretApprovalPolicy.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const plan = await licenseService.getPlan(actorOrgId); @@ -285,13 +288,14 @@ export const secretApprovalPolicyServiceFactory = ({ if (!sapPolicy) throw new NotFoundError({ message: `Secret approval policy with ID '${secretPolicyId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, sapPolicy.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index e1c75b3f9..96ab03b4e 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ProjectMembershipRole, + ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, SecretType, @@ -875,13 +876,14 @@ export const secretApprovalRequestServiceFactory = ({ }: TGenerateSecretApprovalRequestDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath }) @@ -1155,14 +1157,14 @@ export const secretApprovalRequestServiceFactory = ({ if (actor === ActorType.SERVICE || actor === ActorType.Machine) throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); - + ForbidOnInvalidProjectType(ProjectType.SecretManager); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) throw new NotFoundError({ diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts index 6dde2657f..7031d8d12 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-service.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import Ajv from "ajv"; -import { ProjectVersion, TableName } from "@app/db/schemas"; +import { ProjectType, ProjectVersion, TableName } from "@app/db/schemas"; import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TProjectPermission } from "@app/lib/types"; @@ -53,13 +53,14 @@ export const secretRotationServiceFactory = ({ actorAuthMethod, projectId }: TProjectPermission) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); return { @@ -81,13 +82,14 @@ export const secretRotationServiceFactory = ({ secretPath, environment }: TCreateSecretRotationDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation @@ -234,13 +236,14 @@ export const secretRotationServiceFactory = ({ message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation." }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, doc.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation); await secretRotationQueue.removeFromQueue(doc.id, doc.interval); await secretRotationQueue.addToQueue(doc.id, doc.interval); @@ -251,13 +254,14 @@ export const secretRotationServiceFactory = ({ const doc = await secretRotationDAL.findById(rotationId); if (!doc) throw new NotFoundError({ message: `Rotation with ID '${rotationId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, doc.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 481123896..2526facb7 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; +import { ProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { InternalServerError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; @@ -322,13 +322,14 @@ export const secretSnapshotServiceFactory = ({ if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${snapshotId}' not found` }); const shouldUseBridge = snapshot.projectVersion === 3; - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, snapshot.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts index e68eb991a..cd93c40fb 100644 --- a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts +++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts @@ -1,10 +1,10 @@ import { ForbiddenError } from "@casl/ability"; import ms from "ms"; +import { ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; - import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal"; import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal"; import { @@ -53,7 +53,7 @@ export const sshCertificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, @@ -61,6 +61,7 @@ export const sshCertificateTemplateServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SshCertificateTemplates @@ -125,7 +126,7 @@ export const sshCertificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, certTemplate.projectId, @@ -133,6 +134,7 @@ export const sshCertificateTemplateServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.SshCertificateTemplates @@ -193,7 +195,7 @@ export const sshCertificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, certificateTemplate.projectId, @@ -201,6 +203,7 @@ export const sshCertificateTemplateServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.SshCertificateTemplates @@ -219,7 +222,7 @@ export const sshCertificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, certTemplate.projectId, @@ -227,6 +230,7 @@ export const sshCertificateTemplateServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts index 7fd542293..c18e3551e 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-service.ts @@ -1,5 +1,5 @@ import { ForbiddenError } from "@casl/ability"; - +import { ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; @@ -10,7 +10,6 @@ import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certific import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; - import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types"; import { createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns"; import { @@ -66,7 +65,7 @@ export const sshCertificateAuthorityServiceFactory = ({ actor, actorOrgId }: TCreateSshCaDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, @@ -74,6 +73,7 @@ export const sshCertificateAuthorityServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SshCertificateAuthorities @@ -119,7 +119,7 @@ export const sshCertificateAuthorityServiceFactory = ({ const ca = await sshCertificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, @@ -127,6 +127,7 @@ export const sshCertificateAuthorityServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities @@ -189,7 +190,7 @@ export const sshCertificateAuthorityServiceFactory = ({ const ca = await sshCertificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, @@ -197,6 +198,7 @@ export const sshCertificateAuthorityServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, ProjectPermissionSub.SshCertificateAuthorities @@ -228,7 +230,7 @@ export const sshCertificateAuthorityServiceFactory = ({ const ca = await sshCertificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, @@ -236,6 +238,7 @@ export const sshCertificateAuthorityServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, ProjectPermissionSub.SshCertificateAuthorities @@ -270,7 +273,7 @@ export const sshCertificateAuthorityServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, sshCertificateTemplate.projectId, @@ -278,6 +281,7 @@ export const sshCertificateAuthorityServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SshCertificates @@ -394,7 +398,7 @@ export const sshCertificateAuthorityServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, sshCertificateTemplate.projectId, @@ -402,6 +406,7 @@ export const sshCertificateAuthorityServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, ProjectPermissionSub.SshCertificates @@ -493,7 +498,7 @@ export const sshCertificateAuthorityServiceFactory = ({ const ca = await sshCertificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `SSH CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, @@ -501,6 +506,7 @@ export const sshCertificateAuthorityServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ee6eed665..791877173 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -351,6 +351,52 @@ export const OIDC_AUTH = { } } as const; +export const JWT_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + configurationType: "The configuration for validating JWTs. Must be one of: 'jwks', 'static'", + jwksUrl: + "The URL of the JWKS endpoint. Required if configurationType is 'jwks'. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.", + jwksCaCert: "The PEM-encoded CA certificate for validating the TLS connection to the JWKS endpoint.", + publicKeys: + "A list of PEM-encoded public keys used to verify JWT signatures. Required if configurationType is 'static'. Each key must be in RSA or ECDSA format and properly PEM-encoded with BEGIN/END markers.", + boundIssuer: "The unique identifier of the JWT provider.", + boundAudiences: "The list of intended recipients.", + boundClaims: "The attributes that should be present in the JWT for it to be valid.", + boundSubject: "The expected principal that is the subject of the JWT.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", + 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." + }, + UPDATE: { + identityId: "The ID of the identity to update the auth method for.", + configurationType: "The new configuration for validating JWTs. Must be one of: 'jwks', 'static'", + jwksUrl: + "The new URL of the JWKS endpoint. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.", + jwksCaCert: "The new PEM-encoded CA certificate for validating the TLS connection to the JWKS endpoint.", + publicKeys: + "A new list of PEM-encoded public keys used to verify JWT signatures. Each key must be in RSA or ECDSA format and properly PEM-encoded with BEGIN/END markers.", + boundIssuer: "The new unique identifier of the JWT provider.", + boundAudiences: "The new list of intended recipients.", + boundClaims: "The new attributes that should be present in the JWT for it to be valid.", + boundSubject: "The new expected principal that is the subject of the JWT.", + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", + 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." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the auth method for." + }, + REVOKE: { + identityId: "The ID of the identity to revoke the auth method for." + } +} as const; + export const ORGANIZATIONS = { LIST_USER_MEMBERSHIPS: { organizationId: "The ID of the organization to get memberships from." @@ -382,7 +428,8 @@ export const ORGANIZATIONS = { search: "The text string that identity membership names will be filtered by." }, GET_PROJECTS: { - organizationId: "The ID of the organization to get projects from." + organizationId: "The ID of the organization to get projects from.", + type: "The type of project to filter by." }, LIST_GROUPS: { organizationId: "The ID of the organization to list groups for." diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 66c5f3d98..7bb95468a 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -166,8 +166,7 @@ const envSchema = z OTEL_COLLECTOR_BASIC_AUTH_PASSWORD: zpStr(z.string().optional()), OTEL_EXPORT_TYPE: z.enum(["prometheus", "otlp"]).optional(), - PLAIN_API_KEY: zpStr(z.string().optional()), - PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()), + PYLON_API_KEY: zpStr(z.string().optional()), DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"), SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"), WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()), diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 5dccb38d3..1449a0e24 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -128,6 +128,8 @@ import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/i import { identityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service"; import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal"; import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; +import { identityJwtAuthDALFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-dal"; +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 { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal"; @@ -305,6 +307,7 @@ export const registerRoutes = async ( const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityGcpAuthDAL = identityGcpAuthDALFactory(db); const identityOidcAuthDAL = identityOidcAuthDALFactory(db); + const identityJwtAuthDAL = identityJwtAuthDALFactory(db); const identityAzureAuthDAL = identityAzureAuthDALFactory(db); const auditLogDAL = auditLogDALFactory(auditLogDb ?? db); @@ -783,7 +786,8 @@ export const registerRoutes = async ( pkiAlertDAL, pkiCollectionDAL, permissionService, - smtpService + smtpService, + projectDAL }); const pkiCollectionService = pkiCollectionServiceFactory({ @@ -791,7 +795,8 @@ export const registerRoutes = async ( pkiCollectionItemDAL, certificateAuthorityDAL, certificateDAL, - permissionService + permissionService, + projectDAL }); const projectTemplateService = projectTemplateServiceFactory({ @@ -1216,6 +1221,15 @@ export const registerRoutes = async ( orgBotDAL }); + const identityJwtAuthService = identityJwtAuthServiceFactory({ + identityJwtAuthDAL, + permissionService, + identityAccessTokenDAL, + identityOrgMembershipDAL, + licenseService, + kmsService + }); + const dynamicSecretProviders = buildDynamicSecretProviders(); const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ queueService, @@ -1274,7 +1288,8 @@ export const registerRoutes = async ( }); const userEngagementService = userEngagementServiceFactory({ - userDAL + userDAL, + orgDAL }); const slackService = slackServiceFactory({ @@ -1292,7 +1307,8 @@ export const registerRoutes = async ( const cmekService = cmekServiceFactory({ kmsDAL, kmsService, - permissionService + permissionService, + projectDAL }); const externalMigrationQueue = externalMigrationQueueFactory({ @@ -1378,6 +1394,7 @@ export const registerRoutes = async ( identityAwsAuth: identityAwsAuthService, identityAzureAuth: identityAzureAuthService, identityOidcAuth: identityOidcAuthService, + identityJwtAuth: identityJwtAuthService, accessApprovalPolicy: accessApprovalPolicyService, accessApprovalRequest: accessApprovalRequestService, secretApprovalPolicy: secretApprovalPolicyService, diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 69a648d9e..67aee3a1f 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -220,6 +220,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ id: true, name: true, description: true, + type: true, slug: true, autoCapitalization: true, orgId: true, diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts new file mode 100644 index 000000000..d60bb969d --- /dev/null +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -0,0 +1,386 @@ +import { z } from "zod"; + +import { IdentityJwtAuthsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { JWT_AUTH } from "@app/lib/api-docs"; +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 { JwtConfigurationType } from "@app/services/identity-jwt-auth/identity-jwt-auth-types"; +import { + validateJwtAuthAudiencesField, + validateJwtBoundClaimsField +} from "@app/services/identity-jwt-auth/identity-jwt-auth-validators"; + +const IdentityJwtAuthResponseSchema = IdentityJwtAuthsSchema.omit({ + encryptedJwksCaCert: true, + encryptedPublicKeys: true +}).extend({ + jwksCaCert: z.string(), + publicKeys: z.string().array() +}); + +const CreateBaseSchema = z.object({ + boundIssuer: z.string().trim().default("").describe(JWT_AUTH.ATTACH.boundIssuer), + boundAudiences: validateJwtAuthAudiencesField.describe(JWT_AUTH.ATTACH.boundAudiences), + boundClaims: validateJwtBoundClaimsField.describe(JWT_AUTH.ATTACH.boundClaims), + boundSubject: z.string().trim().default("").describe(JWT_AUTH.ATTACH.boundSubject), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(JWT_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(1) + .max(315360000) + .refine((value) => value !== 0, { + message: "accessTokenTTL must have a non zero number" + }) + .default(2592000) + .describe(JWT_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .refine((value) => value !== 0, { + message: "accessTokenMaxTTL must have a non zero number" + }) + .default(2592000) + .describe(JWT_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.ATTACH.accessTokenNumUsesLimit) +}); + +const UpdateBaseSchema = z + .object({ + boundIssuer: z.string().trim().default("").describe(JWT_AUTH.UPDATE.boundIssuer), + boundAudiences: validateJwtAuthAudiencesField.describe(JWT_AUTH.UPDATE.boundAudiences), + boundClaims: validateJwtBoundClaimsField.describe(JWT_AUTH.UPDATE.boundClaims), + boundSubject: z.string().trim().default("").describe(JWT_AUTH.UPDATE.boundSubject), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(JWT_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(1) + .max(315360000) + .refine((value) => value !== 0, { + message: "accessTokenTTL must have a non zero number" + }) + .default(2592000) + .describe(JWT_AUTH.UPDATE.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .refine((value) => value !== 0, { + message: "accessTokenMaxTTL must have a non zero number" + }) + .default(2592000) + .describe(JWT_AUTH.UPDATE.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.UPDATE.accessTokenNumUsesLimit) + }) + .partial(); + +const JwksConfigurationSchema = z.object({ + configurationType: z.literal(JwtConfigurationType.JWKS).describe(JWT_AUTH.ATTACH.configurationType), + jwksUrl: z.string().trim().url().describe(JWT_AUTH.ATTACH.jwksUrl), + jwksCaCert: z.string().trim().default("").describe(JWT_AUTH.ATTACH.jwksCaCert), + publicKeys: z.string().array().optional().default([]).describe(JWT_AUTH.ATTACH.publicKeys) +}); + +const StaticConfigurationSchema = z.object({ + configurationType: z.literal(JwtConfigurationType.STATIC).describe(JWT_AUTH.ATTACH.configurationType), + jwksUrl: z.string().trim().optional().default("").describe(JWT_AUTH.ATTACH.jwksUrl), + jwksCaCert: z.string().trim().optional().default("").describe(JWT_AUTH.ATTACH.jwksCaCert), + publicKeys: z.string().min(1).array().min(1).describe(JWT_AUTH.ATTACH.publicKeys) +}); + +export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/jwt-auth/login", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Login with JWT Auth", + body: z.object({ + identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId), + jwt: z.string().trim() + }), + response: { + 200: z.object({ + accessToken: z.string(), + expiresIn: z.coerce.number(), + accessTokenMaxTTL: z.coerce.number(), + tokenType: z.literal("Bearer") + }) + } + }, + handler: async (req) => { + const { identityJwtAuth, accessToken, identityAccessToken, identityMembershipOrg } = + await server.services.identityJwtAuth.login({ + identityId: req.body.identityId, + jwt: req.body.jwt + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg?.orgId, + event: { + type: EventType.LOGIN_IDENTITY_JWT_AUTH, + metadata: { + identityId: identityJwtAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + identityJwtAuthId: identityJwtAuth.id + } + } + }); + return { + accessToken, + tokenType: "Bearer" as const, + expiresIn: identityJwtAuth.accessTokenTTL, + accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL + }; + } + }); + + server.route({ + method: "POST", + url: "/jwt-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Attach JWT Auth configuration onto identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(JWT_AUTH.ATTACH.identityId) + }), + body: z.discriminatedUnion("configurationType", [ + JwksConfigurationSchema.merge(CreateBaseSchema), + StaticConfigurationSchema.merge(CreateBaseSchema) + ]), + response: { + 200: z.object({ + identityJwtAuth: IdentityJwtAuthResponseSchema + }) + } + }, + handler: async (req) => { + const identityJwtAuth = await server.services.identityJwtAuth.attachJwtAuth({ + 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: identityJwtAuth.orgId, + event: { + type: EventType.ADD_IDENTITY_JWT_AUTH, + metadata: { + identityId: identityJwtAuth.identityId, + configurationType: identityJwtAuth.configurationType, + jwksUrl: identityJwtAuth.jwksUrl, + jwksCaCert: identityJwtAuth.jwksCaCert, + publicKeys: identityJwtAuth.publicKeys, + boundIssuer: identityJwtAuth.boundIssuer, + boundAudiences: identityJwtAuth.boundAudiences, + boundClaims: identityJwtAuth.boundClaims as Record, + boundSubject: identityJwtAuth.boundSubject, + accessTokenTTL: identityJwtAuth.accessTokenTTL, + accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityJwtAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit + } + } + }); + + return { + identityJwtAuth + }; + } + }); + + server.route({ + method: "PATCH", + url: "/jwt-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Update JWT Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(JWT_AUTH.UPDATE.identityId) + }), + body: z.discriminatedUnion("configurationType", [ + JwksConfigurationSchema.merge(UpdateBaseSchema), + StaticConfigurationSchema.merge(UpdateBaseSchema) + ]), + response: { + 200: z.object({ + identityJwtAuth: IdentityJwtAuthResponseSchema + }) + } + }, + handler: async (req) => { + const identityJwtAuth = await server.services.identityJwtAuth.updateJwtAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityJwtAuth.orgId, + event: { + type: EventType.UPDATE_IDENTITY_JWT_AUTH, + metadata: { + identityId: identityJwtAuth.identityId, + configurationType: identityJwtAuth.configurationType, + jwksUrl: identityJwtAuth.jwksUrl, + jwksCaCert: identityJwtAuth.jwksCaCert, + publicKeys: identityJwtAuth.publicKeys, + boundIssuer: identityJwtAuth.boundIssuer, + boundAudiences: identityJwtAuth.boundAudiences, + boundClaims: identityJwtAuth.boundClaims as Record, + boundSubject: identityJwtAuth.boundSubject, + accessTokenTTL: identityJwtAuth.accessTokenTTL, + accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityJwtAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityJwtAuth }; + } + }); + + server.route({ + method: "GET", + url: "/jwt-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Retrieve JWT Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(JWT_AUTH.RETRIEVE.identityId) + }), + response: { + 200: z.object({ + identityJwtAuth: IdentityJwtAuthResponseSchema + }) + } + }, + handler: async (req) => { + const identityJwtAuth = await server.services.identityJwtAuth.getJwtAuth({ + 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: identityJwtAuth.orgId, + event: { + type: EventType.GET_IDENTITY_JWT_AUTH, + metadata: { + identityId: identityJwtAuth.identityId + } + } + }); + + return { identityJwtAuth }; + } + }); + + server.route({ + method: "DELETE", + url: "/jwt-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Delete JWT Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(JWT_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityJwtAuth: IdentityJwtAuthResponseSchema.omit({ + publicKeys: true, + jwksCaCert: true + }) + }) + } + }, + handler: async (req) => { + const identityJwtAuth = await server.services.identityJwtAuth.revokeJwtAuth({ + 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: identityJwtAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_JWT_AUTH, + metadata: { + identityId: identityJwtAuth.identityId + } + } + }); + + return { identityJwtAuth }; + } + }); +}; diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index 15e6eabef..5ec688061 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -328,7 +328,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ authMethods: z.array(z.string()) }), - project: SanitizedProjectSchema.pick({ name: true, id: true }) + project: SanitizedProjectSchema.pick({ name: true, id: true, type: true }) }) ) }) diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index f9edfc18c..a04f77b7a 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -12,6 +12,7 @@ import { registerIdentityAccessTokenRouter } from "./identity-access-token-route import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; 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 { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityRouter } from "./identity-router"; @@ -54,6 +55,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAzureAuthRouter); await authRouter.register(registerIdentityOidcAuthRouter); + await authRouter.register(registerIdentityJwtAuthRouter); }, { prefix: "/auth" } ); diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 5e652283c..e00e06b77 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -97,7 +97,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) bearerAuth: [] } ], - querystring: z.object({ + params: z.object({ integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.UPDATE_BY_ID.integrationAuthId) }), body: z.object({ @@ -126,7 +126,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - integrationAuthId: req.query.integrationAuthId, + integrationAuthId: req.params.integrationAuthId, ...req.body }); diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index f27462d02..68d13842c 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -5,6 +5,7 @@ import { ProjectMembershipsSchema, ProjectRolesSchema, ProjectSlackConfigsSchema, + ProjectType, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -135,7 +136,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { includeRoles: z .enum(["true", "false"]) .default("false") - .transform((value) => value === "true") + .transform((value) => value === "true"), + type: z + .enum([ProjectType.SecretManager, ProjectType.KMS, ProjectType.CertificateManager, ProjectType.SSH, "all"]) + .optional() }), response: { 200: z.object({ @@ -154,7 +158,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actor: req.permission.type, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + type: req.query.type }); return { workspaces }; } diff --git a/backend/src/server/routes/v1/user-engagement-router.ts b/backend/src/server/routes/v1/user-engagement-router.ts index e3ce6532e..1a13dbc6e 100644 --- a/backend/src/server/routes/v1/user-engagement-router.ts +++ b/backend/src/server/routes/v1/user-engagement-router.ts @@ -21,7 +21,7 @@ export const registerUserEngagementRouter = async (server: FastifyZodProvider) = }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - return server.services.userEngagement.createUserWish(req.permission.id, req.body.text); + return server.services.userEngagement.createUserWish(req.permission.id, req.permission.orgId, req.body.text); } }); }; diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 5d34bc702..cb630b143 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -5,6 +5,7 @@ import { OrgMembershipsSchema, ProjectMembershipsSchema, ProjectsSchema, + ProjectType, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -78,6 +79,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { params: z.object({ organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId) }), + querystring: z.object({ + type: z.nativeEnum(ProjectType).optional().describe(ORGANIZATIONS.GET_PROJECTS.type) + }), response: { 200: z.object({ workspaces: z @@ -104,7 +108,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - orgId: req.params.organizationId + orgId: req.params.organizationId, + type: req.query.type }); return { workspaces }; @@ -281,7 +286,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { lastName: true, id: true }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })), - project: ProjectsSchema.pick({ name: true, id: true }), + project: ProjectsSchema.pick({ name: true, id: true, type: true }), roles: z.array( z.object({ id: z.string(), diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 425a7e149..84d2ee6cd 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -5,7 +5,8 @@ import { CertificatesSchema, PkiAlertsSchema, PkiCollectionsSchema, - ProjectKeysSchema + ProjectKeysSchema, + ProjectType } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; @@ -162,7 +163,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { template: slugSchema({ field: "Template Name", max: 64 }) .optional() .default(InfisicalProjectTemplate.Default) - .describe(PROJECTS.CREATE.template) + .describe(PROJECTS.CREATE.template), + type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager) }), response: { 200: z.object({ @@ -181,7 +183,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { workspaceDescription: req.body.projectDescription, slug: req.body.slug, kmsKeyId: req.body.kmsKeyId, - template: req.body.template + template: req.body.template, + type: req.body.type }); await server.services.telemetry.sendPostHogEvents({ diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 06efcf9e3..f61a39a00 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -5,7 +5,7 @@ import crypto, { KeyObject } from "crypto"; import ms from "ms"; import { z } from "zod"; -import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; +import { 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 { getConfig } from "@app/lib/config/env"; @@ -77,7 +77,10 @@ type TCertificateAuthorityServiceFactoryDep = { certificateBodyDAL: Pick; pkiCollectionDAL: Pick; pkiCollectionItemDAL: Pick; - projectDAL: Pick; + projectDAL: Pick< + TProjectDALFactory, + "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId" + >; kmsService: Pick; permissionService: Pick; }; @@ -123,14 +126,24 @@ export const certificateAuthorityServiceFactory = ({ }: TCreateCaDTO) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); + let projectId = project.id; - const { permission } = await permissionService.getProjectPermission( + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, - project.id, + projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -161,7 +174,7 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.create( { - projectId: project.id, + projectId, type, organization, ou, @@ -185,7 +198,7 @@ export const certificateAuthorityServiceFactory = ({ ); const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ - projectId: project.id, + projectId, projectDAL, kmsService }); @@ -323,13 +336,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -348,13 +362,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -434,13 +449,14 @@ export const certificateAuthorityServiceFactory = ({ if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -819,13 +835,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: "CA not found" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -965,13 +982,14 @@ export const certificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -1127,13 +1145,14 @@ export const certificateAuthorityServiceFactory = ({ throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates); @@ -1455,13 +1474,14 @@ export const certificateAuthorityServiceFactory = ({ } if (!dto.isInternal) { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( dto.actor, dto.actorId, ca.projectId, dto.actorAuthMethod, dto.actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index ead3e85cb..c8224d28f 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import bcrypt from "bcrypt"; -import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; +import { ProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -67,13 +67,14 @@ export const certificateTemplateServiceFactory = ({ message: `CA with ID ${caId} not found` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -128,13 +129,14 @@ export const certificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, certTemplate.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -185,13 +187,14 @@ export const certificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, certTemplate.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -252,13 +255,14 @@ export const certificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, certTemplate.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -336,13 +340,14 @@ export const certificateTemplateServiceFactory = ({ }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, certTemplate.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 8dc2de901..3a96ecf91 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; +import { ProjectType } 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"; @@ -49,13 +50,14 @@ export const certificateServiceFactory = ({ const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); @@ -72,13 +74,14 @@ export const certificateServiceFactory = ({ const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); @@ -106,13 +109,14 @@ export const certificateServiceFactory = ({ const cert = await certificateDAL.findOne({ serialNumber }); const ca = await certificateAuthorityDAL.findById(cert.caId); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, ca.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates); diff --git a/backend/src/services/cmek/cmek-service.ts b/backend/src/services/cmek/cmek-service.ts index c8e1b932a..4f14cab66 100644 --- a/backend/src/services/cmek/cmek-service.ts +++ b/backend/src/services/cmek/cmek-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -14,24 +15,33 @@ import { import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "../project/project-dal"; + type TCmekServiceFactoryDep = { kmsService: TKmsServiceFactory; kmsDAL: TKmsKeyDALFactory; permissionService: TPermissionServiceFactory; + projectDAL: Pick; }; export type TCmekServiceFactory = ReturnType; -export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => { - const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => { - const { permission } = await permissionService.getProjectPermission( +export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, projectDAL }: TCmekServiceFactoryDep) => { + const createCmek = async ({ projectId: preSplitProjectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => { + let projectId = preSplitProjectId; + const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + if (cmekProjectFromSplit) { + projectId = cmekProjectFromSplit.id; + } + + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor.type, actor.id, projectId, actor.authMethod, actor.orgId ); - + ForbidOnInvalidProjectType(ProjectType.KMS); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek); const cmek = await kmsService.generateKmsKey({ @@ -50,13 +60,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor.type, actor.id, key.projectId, actor.authMethod, actor.orgId ); + ForbidOnInvalidProjectType(ProjectType.KMS); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek); @@ -72,13 +83,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor.type, actor.id, key.projectId, actor.authMethod, actor.orgId ); + ForbidOnInvalidProjectType(ProjectType.KMS); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek); @@ -87,7 +99,16 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC return cmek; }; - const listCmeksByProjectId = async ({ projectId, ...filters }: TListCmeksByProjectIdDTO, actor: OrgServiceActor) => { + const listCmeksByProjectId = async ( + { projectId: preSplitProjectId, ...filters }: TListCmeksByProjectIdDTO, + actor: OrgServiceActor + ) => { + let projectId = preSplitProjectId; + const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(preSplitProjectId, ProjectType.KMS); + if (cmekProjectFromSplit) { + projectId = cmekProjectFromSplit.id; + } + const { permission } = await permissionService.getProjectPermission( actor.type, actor.id, @@ -112,7 +133,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor.type, actor.id, key.projectId, @@ -120,6 +141,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC actor.orgId ); + ForbidOnInvalidProjectType(ProjectType.KMS); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek); const encrypt = await kmsService.encryptWithKmsKey({ kmsId: keyId }); @@ -138,13 +160,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor.type, actor.id, key.projectId, actor.authMethod, actor.orgId ); + ForbidOnInvalidProjectType(ProjectType.KMS); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek); 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 f12bd8c15..57517c706 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 @@ -37,7 +37,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { ) .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`) .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) - + .leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`) .select(selectAllTableCols(TableName.IdentityAccessToken)) .select( db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), @@ -47,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"), 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("name").withSchema(TableName.Identity) ) .first(); @@ -61,7 +62,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure, trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s, trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc, - trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken + trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken, + trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt }; } 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 a59d1e959..47d1791d2 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 @@ -171,7 +171,8 @@ export const identityAccessTokenServiceFactory = ({ [IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth, [IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth, [IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth, - [IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth + [IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth, + [IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth }; const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod]; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-dal.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-dal.ts new file mode 100644 index 000000000..5e6d13be6 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-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 TIdentityJwtAuthDALFactory = ReturnType; + +export const identityJwtAuthDALFactory = (db: TDbClient) => { + const jwtAuthOrm = ormify(db, TableName.IdentityJwtAuth); + + return jwtAuthOrm; +}; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-fns.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-fns.ts new file mode 100644 index 000000000..57aa933d6 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-fns.ts @@ -0,0 +1,13 @@ +import picomatch from "picomatch"; + +export const doesFieldValueMatchJwtPolicy = (fieldValue: string | boolean | number, policyValue: string) => { + if (typeof fieldValue === "boolean") { + return fieldValue === (policyValue === "true"); + } + + if (typeof fieldValue === "number") { + return fieldValue === parseInt(policyValue, 10); + } + + return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue); +}; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts new file mode 100644 index 000000000..5f8fc5ff6 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -0,0 +1,534 @@ +import { ForbiddenError } from "@casl/ability"; +import https from "https"; +import jwt from "jsonwebtoken"; +import { JwksClient } from "jwks-rsa"; + +import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } 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 { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; + +import { ActorType, AuthTokenType } from "../auth/auth-type"; +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 { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal"; +import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns"; +import { + JwtConfigurationType, + TAttachJwtAuthDTO, + TGetJwtAuthDTO, + TLoginJwtAuthDTO, + TRevokeJwtAuthDTO, + TUpdateJwtAuthDTO +} from "./identity-jwt-auth-types"; + +type TIdentityJwtAuthServiceFactoryDep = { + identityJwtAuthDAL: TIdentityJwtAuthDALFactory; + identityOrgMembershipDAL: Pick; + identityAccessTokenDAL: Pick; + permissionService: Pick; + licenseService: Pick; + kmsService: Pick; +}; + +export type TIdentityJwtAuthServiceFactory = ReturnType; + +export const identityJwtAuthServiceFactory = ({ + identityJwtAuthDAL, + identityOrgMembershipDAL, + permissionService, + licenseService, + identityAccessTokenDAL, + kmsService +}: TIdentityJwtAuthServiceFactoryDep) => { + const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => { + const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); + if (!identityJwtAuth) { + throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" }); + } + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ + identityId: identityJwtAuth.identityId + }); + if (!identityMembershipOrg) { + throw new NotFoundError({ + message: `Identity organization membership for identity with ID '${identityJwtAuth.identityId}' not found` + }); + } + + const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityMembershipOrg.orgId + }); + + const decodedToken = jwt.decode(jwtValue, { complete: true }); + if (!decodedToken) { + throw new UnauthorizedError({ + message: "Invalid JWT" + }); + } + + let tokenData: Record = {}; + + if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) { + const decryptedJwksCaCert = orgDataKeyDecryptor({ + cipherTextBlob: identityJwtAuth.encryptedJwksCaCert + }).toString(); + const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert }); + const client = new JwksClient({ + jwksUri: identityJwtAuth.jwksUrl, + requestAgent + }); + + const { kid } = decodedToken.header; + const jwtSigningKey = await client.getSigningKey(kid); + + try { + tokenData = jwt.verify(jwtValue, jwtSigningKey.getPublicKey()) as Record; + } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + throw new UnauthorizedError({ + message: `Access denied: ${error.message}` + }); + } + + throw error; + } + } else { + const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys }) + .toString() + .split(","); + + const errors: string[] = []; + let isMatchAnyKey = false; + for (const publicKey of decryptedPublicKeys) { + try { + tokenData = jwt.verify(jwtValue, publicKey) as Record; + isMatchAnyKey = true; + } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + errors.push(error.message); + } + } + } + + if (!isMatchAnyKey) { + throw new UnauthorizedError({ + message: `Access denied: JWT verification failed with all keys. Errors - ${errors.join("; ")}` + }); + } + } + + if (identityJwtAuth.boundIssuer) { + if (tokenData.iss !== identityJwtAuth.boundIssuer) { + throw new ForbiddenRequestError({ + message: "Access denied: issuer mismatch" + }); + } + } + + if (identityJwtAuth.boundSubject) { + if (!tokenData.sub) { + throw new UnauthorizedError({ + message: "Access denied: token has no subject field" + }); + } + + if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) { + throw new ForbiddenRequestError({ + message: "Access denied: subject not allowed" + }); + } + } + + if (identityJwtAuth.boundAudiences) { + if (!tokenData.aud) { + throw new UnauthorizedError({ + message: "Access denied: token has no audience field" + }); + } + + if ( + !identityJwtAuth.boundAudiences + .split(", ") + .some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue)) + ) { + throw new UnauthorizedError({ + message: "Access denied: token audience not allowed" + }); + } + } + + if (identityJwtAuth.boundClaims) { + Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => { + const claimValue = (identityJwtAuth.boundClaims as Record)[claimKey]; + + if (!tokenData[claimKey]) { + throw new UnauthorizedError({ + message: `Access denied: token has no ${claimKey} field` + }); + } + + // handle both single and multi-valued claims + if ( + !claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(tokenData[claimKey], claimEntry)) + ) { + throw new UnauthorizedError({ + message: `Access denied: claim mismatch for field ${claimKey}` + }); + } + }); + } + + const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityJwtAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityJwtAuth.accessTokenTTL, + accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.JWT_AUTH + }, + tx + ); + + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityJwtAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + { + expiresIn: + Number(identityAccessToken.accessTokenMaxTTL) === 0 + ? undefined + : Number(identityAccessToken.accessTokenMaxTTL) + } + ); + + return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg }; + }; + + const attachJwtAuth = async ({ + identityId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TAttachJwtAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) { + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + } + if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { + throw new BadRequestError({ + message: "Failed to add JWT 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(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + 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); + }); + + const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({ + plainText: Buffer.from(jwksCaCert) + }); + + const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({ + plainText: Buffer.from(publicKeys.join(",")) + }); + + const identityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => { + const doc = await identityJwtAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + configurationType, + jwksUrl, + encryptedJwksCaCert, + encryptedPublicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + + return doc; + }); + return { ...identityJwtAuth, orgId: identityMembershipOrg.orgId, jwksCaCert, publicKeys }; + }; + + const updateJwtAuth = async ({ + identityId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateJwtAuthDTO) => { + 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.JWT_AUTH)) { + throw new BadRequestError({ + message: "Failed to update JWT Auth" + }); + } + + const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityJwtAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityJwtAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityJwtAuth.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(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + 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); + }); + + const updateQuery: TIdentityJwtAuthsUpdate = { + boundIssuer, + configurationType, + jwksUrl, + boundAudiences, + boundClaims, + boundSubject, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }; + + const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } = + await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + if (jwksCaCert !== undefined) { + const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({ + plainText: Buffer.from(jwksCaCert) + }); + + updateQuery.encryptedJwksCaCert = encryptedJwksCaCert; + } + + if (publicKeys) { + const { cipherTextBlob: encryptedPublicKeys } = orgDataKeyEncryptor({ + plainText: Buffer.from(publicKeys.join(",")) + }); + + updateQuery.encryptedPublicKeys = encryptedPublicKeys; + } + + const updatedJwtAuth = await identityJwtAuthDAL.updateById(identityJwtAuth.id, updateQuery); + const decryptedJwksCaCert = orgDataKeyDecryptor({ cipherTextBlob: updatedJwtAuth.encryptedJwksCaCert }).toString(); + const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: updatedJwtAuth.encryptedPublicKeys }) + .toString() + .split(","); + + return { + ...updatedJwtAuth, + orgId: identityMembershipOrg.orgId, + jwksCaCert: decryptedJwksCaCert, + publicKeys: decryptedPublicKeys + }; + }; + + const getJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetJwtAuthDTO) => { + 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.JWT_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have JWT Auth attached" + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + + const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); + + const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + + const decryptedJwksCaCert = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedJwksCaCert }).toString(); + const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys }) + .toString() + .split(","); + + return { + ...identityJwtAuth, + orgId: identityMembershipOrg.orgId, + jwksCaCert: decryptedJwksCaCert, + publicKeys: decryptedPublicKeys + }; + }; + + const revokeJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeJwtAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) { + throw new NotFoundError({ message: "Failed to find identity" }); + } + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have JWT auth" + }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + if (!isAtLeastAsPrivileged(permission, rolePermission)) { + throw new ForbiddenRequestError({ + message: "Failed to revoke JWT auth of identity with more privileged role" + }); + } + + const revokedIdentityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => { + const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.JWT_AUTH }, tx); + + return { ...deletedJwtAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + + return revokedIdentityJwtAuth; + }; + + return { + login, + attachJwtAuth, + updateJwtAuth, + getJwtAuth, + revokeJwtAuth + }; +}; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts new file mode 100644 index 000000000..a6881f0e5 --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts @@ -0,0 +1,51 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum JwtConfigurationType { + JWKS = "jwks", + STATIC = "static" +} + +export type TAttachJwtAuthDTO = { + identityId: string; + configurationType: JwtConfigurationType; + jwksUrl: string; + jwksCaCert: string; + publicKeys: string[]; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; +} & Omit; + +export type TUpdateJwtAuthDTO = { + identityId: string; + configurationType?: JwtConfigurationType; + jwksUrl?: string; + jwksCaCert?: string; + publicKeys?: string[]; + boundIssuer?: string; + boundAudiences?: string; + boundClaims?: Record; + boundSubject?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { ipAddress: string }[]; +} & Omit; + +export type TGetJwtAuthDTO = { + identityId: string; +} & Omit; + +export type TRevokeJwtAuthDTO = { + identityId: string; +} & Omit; + +export type TLoginJwtAuthDTO = { + identityId: string; + jwt: string; +}; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-validators.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-validators.ts new file mode 100644 index 000000000..515c2ac7e --- /dev/null +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-validators.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const validateJwtAuthAudiencesField = z + .string() + .trim() + .default("") + .transform((data) => { + if (data === "") return ""; + return data + .split(",") + .map((id) => id.trim()) + .join(", "); + }); + +export const validateJwtBoundClaimsField = z.record(z.string()).transform((data) => { + const formattedClaims: Record = {}; + Object.keys(data).forEach((key) => { + formattedClaims[key] = data[key] + .split(",") + .map((id) => id.trim()) + .join(", "); + }); + + return formattedClaims; +}); diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index fd8eaa15d..bc4f4a303 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -102,6 +102,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), db.ref("projectId").withSchema(TableName.IdentityProjectMembership), db.ref("name").as("projectName").withSchema(TableName.Project), + db.ref("type").as("projectType").withSchema(TableName.Project), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), @@ -126,7 +127,8 @@ export const identityProjectDALFactory = (db: TDbClient) => { createdAt, updatedAt, projectId, - projectName + projectName, + projectType }) => ({ id, identityId, @@ -147,7 +149,8 @@ export const identityProjectDALFactory = (db: TDbClient) => { }, project: { id: projectId, - name: projectName + name: projectName, + type: projectType } }), key: "id", diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts index 49cf4d119..2d77e6544 100644 --- a/backend/src/services/identity/identity-fns.ts +++ b/backend/src/services/identity/identity-fns.ts @@ -7,7 +7,8 @@ export const buildAuthMethods = ({ kubernetesId, oidcId, azureId, - tokenId + tokenId, + jwtId }: { uaId?: string; gcpId?: string; @@ -16,6 +17,7 @@ export const buildAuthMethods = ({ oidcId?: string; azureId?: string; tokenId?: string; + jwtId?: string; }) => { return [ ...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null], @@ -24,6 +26,7 @@ export const buildAuthMethods = ({ ...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null], ...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null], ...[azureId ? IdentityAuthMethod.AZURE_AUTH : null], - ...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null] + ...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null], + ...[jwtId ? IdentityAuthMethod.JWT_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 bbdf96a2b..92a6795d0 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -6,6 +6,7 @@ import { TIdentityAwsAuths, TIdentityAzureAuths, TIdentityGcpAuths, + TIdentityJwtAuths, TIdentityKubernetesAuths, TIdentityOidcAuths, TIdentityOrgMemberships, @@ -70,6 +71,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { `${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityTokenAuth}.identityId` ) + .leftJoin( + TableName.IdentityJwtAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityJwtAuth}.identityId` + ) .select( selectAllTableCols(TableName.IdentityOrgMembership), @@ -81,6 +87,7 @@ 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("name").withSchema(TableName.Identity) ); @@ -183,6 +190,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { "paginatedIdentity.identityId", `${TableName.IdentityTokenAuth}.identityId` ) + .leftJoin( + TableName.IdentityJwtAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityJwtAuth}.identityId` + ) .select( db.ref("id").withSchema("paginatedIdentity"), @@ -200,7 +212,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), 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("tokenId").withSchema(TableName.IdentityTokenAuth), + db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth) ) // cr stands for custom role .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) @@ -237,6 +250,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { uaId, awsId, gcpId, + jwtId, kubernetesId, oidcId, azureId, @@ -271,7 +285,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { kubernetesId, oidcId, azureId, - tokenId + tokenId, + jwtId }) } }), diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 42a3f038b..b4bbbd7cb 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -4,7 +4,13 @@ import { Octokit } from "@octokit/rest"; import { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client"; import AWS from "aws-sdk"; -import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; +import { + ProjectType, + SecretEncryptionAlgo, + SecretKeyEncoding, + TIntegrationAuths, + TIntegrationAuthsInsert +} from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; @@ -145,13 +151,14 @@ export const integrationAuthServiceFactory = ({ if (!Object.values(Integrations).includes(integration as Integrations)) throw new BadRequestError({ message: "Invalid integration" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); const tokenExchange = await exchangeCode({ integration, code, url, installationId }); @@ -254,13 +261,14 @@ export const integrationAuthServiceFactory = ({ if (!Object.values(Integrations).includes(integration as Integrations)) throw new BadRequestError({ message: "Invalid integration" }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); const updateDoc: TIntegrationAuthsInsert = { diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index a990b1ca6..36a4156e7 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { NotFoundError } from "@app/lib/errors"; @@ -80,13 +81,14 @@ export const integrationServiceFactory = ({ if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${integrationAuthId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, integrationAuth.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan( @@ -158,13 +160,14 @@ export const integrationServiceFactory = ({ const integration = await integrationDAL.findById(id); if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, integration.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); const newEnvironment = environment || integration.environment.slug; @@ -293,13 +296,14 @@ export const integrationServiceFactory = ({ const integration = await integrationDAL.findById(id); if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, integration.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 9741220f8..33931bf26 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -15,7 +15,6 @@ import { TProjectUserMembershipRolesInsert, TUsers } from "@app/db/schemas"; -import { TProjects } from "@app/db/schemas/projects"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; @@ -196,26 +195,18 @@ export const orgServiceFactory = ({ return org; }; - const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => { - const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id)); - - let workspaces: (TProjects & { organization: string } & { - environments: { - id: string; - slug: string; - name: string; - }[]; - })[]; - + const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => { if (actor === ActorType.USER) { - workspaces = await projectDAL.findAllProjects(actorId); - } else if (actor === ActorType.IDENTITY) { - workspaces = await projectDAL.findAllProjectsByIdentity(actorId); - } else { - throw new BadRequestError({ message: "Invalid actor type" }); + const workspaces = await projectDAL.findAllProjects(actorId, orgId, type || "all"); + return workspaces; } - return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id)); + if (actor === ActorType.IDENTITY) { + const workspaces = await projectDAL.findAllProjectsByIdentity(actorId, type); + return workspaces; + } + + throw new BadRequestError({ message: "Invalid actor type" }); }; const addGhostUser = async (orgId: string, tx?: Knex) => { diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 05df9429e..1df4b422b 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -1,3 +1,4 @@ +import { ProjectType } from "@app/db/schemas"; import { TOrgPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type"; @@ -55,6 +56,7 @@ export type TFindAllWorkspacesDTO = { actorOrgId: string | undefined; actorAuthMethod: ActorAuthMethod; orgId: string; + type?: ProjectType; }; export type TUpdateOrgDTO = { diff --git a/backend/src/services/pki-alert/pki-alert-service.ts b/backend/src/services/pki-alert/pki-alert-service.ts index 1e7d26825..f002c393b 100644 --- a/backend/src/services/pki-alert/pki-alert-service.ts +++ b/backend/src/services/pki-alert/pki-alert-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -8,6 +9,7 @@ import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-colle import { pkiItemTypeToNameMap } from "@app/services/pki-collection/pki-collection-types"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TProjectDALFactory } from "../project/project-dal"; import { TPkiAlertDALFactory } from "./pki-alert-dal"; import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types"; @@ -19,6 +21,7 @@ type TPkiAlertServiceFactoryDep = { pkiCollectionDAL: Pick; permissionService: Pick; smtpService: Pick; + projectDAL: Pick; }; export type TPkiAlertServiceFactory = ReturnType; @@ -27,7 +30,8 @@ export const pkiAlertServiceFactory = ({ pkiAlertDAL, pkiCollectionDAL, permissionService, - smtpService + smtpService, + projectDAL }: TPkiAlertServiceFactoryDep) => { const sendPkiItemExpiryNotices = async () => { const allAlertItems = await pkiAlertDAL.getExpiringPkiCollectionItemsForAlerting(); @@ -63,7 +67,7 @@ export const pkiAlertServiceFactory = ({ }; const createPkiAlert = async ({ - projectId, + projectId: preSplitProjectId, name, pkiCollectionId, alertBeforeDays, @@ -73,13 +77,23 @@ export const pkiAlertServiceFactory = ({ actor, actorOrgId }: TCreateAlertDTO) => { - const { permission } = await permissionService.getProjectPermission( + let projectId = preSplitProjectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts); @@ -128,13 +142,14 @@ export const pkiAlertServiceFactory = ({ let alert = await pkiAlertDAL.findById(alertId); if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, alert.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts); @@ -160,13 +175,14 @@ export const pkiAlertServiceFactory = ({ let alert = await pkiAlertDAL.findById(alertId); if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, alert.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts); alert = await pkiAlertDAL.deleteById(alertId); diff --git a/backend/src/services/pki-collection/pki-collection-service.ts b/backend/src/services/pki-collection/pki-collection-service.ts index ef849c54f..93b3b65b6 100644 --- a/backend/src/services/pki-collection/pki-collection-service.ts +++ b/backend/src/services/pki-collection/pki-collection-service.ts @@ -1,12 +1,13 @@ import { ForbiddenError } from "@casl/ability"; -import { TPkiCollectionItems } from "@app/db/schemas"; +import { ProjectType, TPkiCollectionItems } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { TProjectDALFactory } from "../project/project-dal"; import { TPkiCollectionDALFactory } from "./pki-collection-dal"; import { transformPkiCollectionItem } from "./pki-collection-fns"; import { TPkiCollectionItemDALFactory } from "./pki-collection-item-dal"; @@ -30,6 +31,7 @@ type TPkiCollectionServiceFactoryDep = { certificateAuthorityDAL: Pick; certificateDAL: Pick; permissionService: Pick; + projectDAL: Pick; }; export type TPkiCollectionServiceFactory = ReturnType; @@ -39,24 +41,35 @@ export const pkiCollectionServiceFactory = ({ pkiCollectionItemDAL, certificateAuthorityDAL, certificateDAL, - permissionService + permissionService, + projectDAL }: TPkiCollectionServiceFactoryDep) => { const createPkiCollection = async ({ name, description, - projectId, + projectId: preSplitProjectId, actorId, actorAuthMethod, actor, actorOrgId }: TCreatePkiCollectionDTO) => { - const { permission } = await permissionService.getProjectPermission( + let projectId = preSplitProjectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -106,13 +119,14 @@ export const pkiCollectionServiceFactory = ({ let pkiCollection = await pkiCollectionDAL.findById(collectionId); if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, pkiCollection.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections); pkiCollection = await pkiCollectionDAL.updateById(collectionId, { @@ -133,13 +147,14 @@ export const pkiCollectionServiceFactory = ({ let pkiCollection = await pkiCollectionDAL.findById(collectionId); if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, pkiCollection.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, @@ -205,13 +220,14 @@ export const pkiCollectionServiceFactory = ({ const pkiCollection = await pkiCollectionDAL.findById(collectionId); if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, pkiCollection.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -298,13 +314,14 @@ export const pkiCollectionServiceFactory = ({ if (!pkiCollectionItem) throw new NotFoundError({ message: `PKI collection item with ID '${itemId}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, pkiCollection.projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts index a54e8de43..5c3fdba32 100644 --- a/backend/src/services/project-env/project-env-service.ts +++ b/backend/src/services/project-env/project-env-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ProjectType } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -41,13 +42,14 @@ export const projectEnvServiceFactory = ({ name, slug }: TCreateEnvDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); const lock = await keyStore @@ -129,13 +131,14 @@ export const projectEnvServiceFactory = ({ id, position }: TUpdateEnvDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); const lock = await keyStore @@ -192,13 +195,14 @@ export const projectEnvServiceFactory = ({ }; const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); const lock = await keyStore diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index bfd0c6f85..61b703e70 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -217,20 +217,33 @@ export const projectMembershipDALFactory = (db: TDbClient) => { db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), - db.ref("id").as("projectId").withSchema(TableName.Project) + db.ref("id").as("projectId").withSchema(TableName.Project), + db.ref("type").as("projectType").withSchema(TableName.Project) ) .where({ isGhost: false }); const members = sqlNestRelationships({ data: docs, - parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, projectId, projectName }) => ({ + parentMapper: ({ + email, + firstName, + username, + lastName, + publicKey, + isGhost, + id, + projectId, + projectName, + projectType + }) => ({ id, userId, projectId, user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }, project: { id: projectId, - name: projectName + name: projectName, + type: projectType } }), key: "id", diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index e5e447145..992789da2 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -1,7 +1,14 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas"; +import { + ProjectsSchema, + ProjectType, + ProjectUpgradeStatus, + ProjectVersion, + TableName, + TProjectsUpdate +} from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; @@ -12,12 +19,18 @@ export type TProjectDALFactory = ReturnType; export const projectDALFactory = (db: TDbClient) => { const projectOrm = ormify(db, TableName.Project); - const findAllProjects = async (userId: string) => { + const findAllProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => { try { const workspaces = await db .replicaNode()(TableName.ProjectMembership) .where({ userId }) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + if (projectType !== "all") { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), @@ -31,14 +44,17 @@ export const projectDALFactory = (db: TDbClient) => { { column: `${TableName.Environment}.position`, order: "asc" } ]); - const groups: string[] = await db(TableName.UserGroupMembership) - .where({ userId }) - .select(selectAllTableCols(TableName.UserGroupMembership)) - .pluck("groupId"); + const groups = db(TableName.UserGroupMembership).where({ userId }).select("groupId"); const groupWorkspaces = await db(TableName.GroupProjectMembership) .whereIn("groupId", groups) .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + if (projectType) { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .whereNotIn( `${TableName.Project}.id`, workspaces.map(({ id }) => id) @@ -108,12 +124,17 @@ export const projectDALFactory = (db: TDbClient) => { } }; - const findAllProjectsByIdentity = async (identityId: string) => { + const findAllProjectsByIdentity = async (identityId: string, projectType?: ProjectType) => { try { const workspaces = await db .replicaNode()(TableName.IdentityProjectMembership) .where({ identityId }) .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + .andWhere((qb) => { + if (projectType) { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), @@ -315,6 +336,22 @@ export const projectDALFactory = (db: TDbClient) => { }; }; + const getProjectFromSplitId = async (projectId: string, projectType: ProjectType) => { + try { + const project = await db(TableName.ProjectSplitBackfillIds) + .where({ + sourceProjectId: projectId, + destinationProjectType: projectType + }) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectSplitBackfillIds}.destinationProjectId`) + .select(selectAllTableCols(TableName.Project)) + .first(); + return project; + } catch (error) { + throw new DatabaseError({ error, name: `Failed to find split project with id ${projectId}` }); + } + }; + return { ...projectOrm, findAllProjects, @@ -325,6 +362,7 @@ export const projectDALFactory = (db: TDbClient) => { findProjectByFilter, findProjectBySlug, findProjectWithOrg, - checkProjectUpgradeStatus + checkProjectUpgradeStatus, + getProjectFromSplitId }; }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index e7742e4f7..f2b8f829e 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { ProjectMembershipRole, ProjectVersion, TProjectEnvironments } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectType, ProjectVersion, TProjectEnvironments } 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"; @@ -165,10 +165,10 @@ export const projectServiceFactory = ({ kmsKeyId, tx: trx, createDefaultEnvs = true, - template = InfisicalProjectTemplate.Default + template = InfisicalProjectTemplate.Default, + type = ProjectType.SecretManager }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); - const { permission, membership: orgMembership } = await permissionService.getOrgPermission( actor, actorId, @@ -218,6 +218,7 @@ export const projectServiceFactory = ({ const project = await projectDAL.create( { name: workspaceName, + type, description: workspaceDescription, orgId: organization.id, slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`), @@ -442,8 +443,14 @@ export const projectServiceFactory = ({ return deletedProject; }; - const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => { - const workspaces = await projectDAL.findAllProjects(actorId); + const getProjects = async ({ + actorId, + includeRoles, + actorAuthMethod, + actorOrgId, + type = ProjectType.SecretManager + }: TListProjectsDTO) => { + const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type); if (includeRoles) { const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod); @@ -693,11 +700,19 @@ export const projectServiceFactory = ({ actor }: TListProjectCasDTO) => { const project = await projectDAL.findProjectByFilter(filter); + let projectId = project.id; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } const { permission } = await permissionService.getProjectPermission( actor, actorId, - project.id, + projectId, actorAuthMethod, actorOrgId ); @@ -709,7 +724,7 @@ export const projectServiceFactory = ({ const cas = await certificateAuthorityDAL.find( { - projectId: project.id, + projectId, ...(status && { status }), ...(friendlyName && { friendlyName }), ...(commonName && { commonName }) @@ -735,18 +750,27 @@ export const projectServiceFactory = ({ actor }: TListProjectCertsDTO) => { const project = await projectDAL.findProjectByFilter(filter); + let projectId = project.id; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, - project.id, + projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates); - const cas = await certificateAuthorityDAL.find({ projectId: project.id }); + const cas = await certificateAuthorityDAL.find({ projectId }); const certificates = await certificateDAL.find( { @@ -760,7 +784,7 @@ export const projectServiceFactory = ({ ); const count = await certificateDAL.countCertificatesInProject({ - projectId: project.id, + projectId, friendlyName, commonName }); @@ -775,19 +799,29 @@ export const projectServiceFactory = ({ * Return list of (PKI) alerts configured for project */ const listProjectAlerts = async ({ - projectId, + projectId: preSplitProjectId, actor, actorId, actorAuthMethod, actorOrgId }: TListProjectAlertsDTO) => { - const { permission } = await permissionService.getProjectPermission( + let projectId = preSplitProjectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); @@ -802,19 +836,28 @@ export const projectServiceFactory = ({ * Return list of PKI collections for project */ const listProjectPkiCollections = async ({ - projectId, + projectId: preSplitProjectId, actor, actorId, actorAuthMethod, actorOrgId }: TListProjectAlertsDTO) => { - const { permission } = await permissionService.getProjectPermission( + let projectId = preSplitProjectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); @@ -829,19 +872,29 @@ export const projectServiceFactory = ({ * Return list of certificate templates for project */ const listProjectCertificateTemplates = async ({ - projectId, + projectId: preSplitProjectId, actorId, actorOrgId, actorAuthMethod, actor }: TListProjectCertificateTemplatesDTO) => { - const { permission } = await permissionService.getProjectPermission( + let projectId = preSplitProjectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.CertificateManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, @@ -865,7 +918,7 @@ export const projectServiceFactory = ({ actor, projectId }: TListProjectSshCasDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, @@ -873,6 +926,7 @@ export const projectServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities @@ -900,7 +954,7 @@ export const projectServiceFactory = ({ actor, projectId }: TListProjectSshCertificatesDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, @@ -908,6 +962,7 @@ export const projectServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); const cas = await sshCertificateAuthorityDAL.find({ @@ -938,7 +993,7 @@ export const projectServiceFactory = ({ actor, projectId }: TListProjectSshCertificateTemplatesDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, @@ -946,6 +1001,7 @@ export const projectServiceFactory = ({ actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SSH); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 6e9d85eb0..2c6b8e2da 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { TProjectKeys } from "@app/db/schemas"; +import { ProjectType, TProjectKeys } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; @@ -35,6 +35,7 @@ export type TCreateProjectDTO = { createDefaultEnvs?: boolean; template?: string; tx?: Knex; + type?: ProjectType; }; export type TDeleteProjectBySlugDTO = { @@ -84,6 +85,7 @@ export type TDeleteProjectDTO = { export type TListProjectsDTO = { includeRoles: boolean; + type?: ProjectType | "all"; } & Omit; export type TUpgradeProjectDTO = { diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index d787520a2..99bd81d6d 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import path from "path"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; -import { TSecretFoldersInsert } from "@app/db/schemas"; +import { ProjectType, TSecretFoldersInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; @@ -52,13 +52,14 @@ export const secretFolderServiceFactory = ({ environment, path: secretPath }: TCreateFolderDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, @@ -150,13 +151,14 @@ export const secretFolderServiceFactory = ({ throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, project.id, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); folders.forEach(({ environment, path: secretPath }) => { ForbiddenError.from(permission).throwUnlessCan( @@ -259,13 +261,14 @@ export const secretFolderServiceFactory = ({ path: secretPath, id }: TUpdateFolderDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, @@ -339,13 +342,14 @@ export const secretFolderServiceFactory = ({ path: secretPath, idOrName }: TDeleteFolderDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 25e78fb65..bb2c54372 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -2,7 +2,7 @@ import path from "node:path"; import { ForbiddenError, subject } from "@casl/ability"; -import { TableName } from "@app/db/schemas"; +import { ProjectType, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -73,13 +73,14 @@ export const secretImportServiceFactory = ({ isReplication, path: secretPath }: TCreateSecretImportDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); // check if user has permission to import into destination path ForbiddenError.from(permission).throwUnlessCan( @@ -189,13 +190,15 @@ export const secretImportServiceFactory = ({ data, id }: TUpdateSecretImportDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) @@ -283,13 +286,15 @@ export const secretImportServiceFactory = ({ actorAuthMethod, id }: TDeleteSecretImportDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) diff --git a/backend/src/services/secret-tag/secret-tag-service.ts b/backend/src/services/secret-tag/secret-tag-service.ts index 6cae3997a..02d2b5af0 100644 --- a/backend/src/services/secret-tag/secret-tag-service.ts +++ b/backend/src/services/secret-tag/secret-tag-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; @@ -23,7 +24,7 @@ export type TSecretTagServiceFactory = ReturnType { const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, @@ -31,6 +32,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); + ForbidOnInvalidProjectType(ProjectType.SecretManager); const existingTag = await secretTagDAL.findOne({ slug, projectId }); if (existingTag) throw new BadRequestError({ message: "Tag already exist" }); @@ -54,7 +56,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" }); } - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, tag.projectId, @@ -62,6 +64,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); + ForbidOnInvalidProjectType(ProjectType.SecretManager); const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug }); return updatedTag; @@ -71,7 +74,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe const tag = await secretTagDAL.findById(id); if (!tag) throw new NotFoundError({ message: `Tag with ID '${id}' not found` }); - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, tag.projectId, @@ -79,6 +82,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); + ForbidOnInvalidProjectType(ProjectType.SecretManager); const deletedTag = await secretTagDAL.deleteById(tag.id); return deletedTag; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 0803bda35..e13a77432 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError, PureAbility, subject } from "@casl/ability"; import { z } from "zod"; -import { ProjectMembershipRole, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectType, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -188,13 +188,14 @@ export const secretV2BridgeServiceFactory = ({ secretPath, ...inputSecret }: TCreateSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -310,13 +311,14 @@ export const secretV2BridgeServiceFactory = ({ secretPath, ...inputSecret }: TUpdateSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); if (inputSecret.newSecretName === "") { throw new BadRequestError({ message: "New secret name cannot be empty" }); @@ -494,13 +496,14 @@ export const secretV2BridgeServiceFactory = ({ secretPath, ...inputSecret }: TDeleteSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1081,13 +1084,14 @@ export const secretV2BridgeServiceFactory = ({ projectId, secrets: inputSecrets }: TCreateManySecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1221,13 +1225,14 @@ export const secretV2BridgeServiceFactory = ({ secretPath, secrets: inputSecrets }: TUpdateManySecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1427,13 +1432,14 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteManySecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -1569,13 +1575,14 @@ export const secretV2BridgeServiceFactory = ({ actorOrgId, actorAuthMethod }: TBackFillSecretReferencesDTO) => { - const { hasRole } = await permissionService.getProjectPermission( + const { hasRole, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); if (!hasRole(ProjectMembershipRole.Admin)) throw new ForbiddenRequestError({ message: "Only admins are allowed to take this action" }); @@ -1616,13 +1623,14 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, actorOrgId }: TMoveSecretsDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath); if (!sourceFolder) { diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index a0bce5371..7772b8518 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -20,7 +20,8 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { .join(TableName.SecretV2, `${TableName.SecretV2}.id`, `${TableName.SecretVersionV2}.secretId`) .join( (tx || db)(TableName.SecretVersionV2) - .groupBy("folderId", "secretId") + .where(`${TableName.SecretVersionV2}.folderId`, folderId) + .groupBy("secretId") .max("version") .select("secretId") .as("latestVersion"), diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index d62d09f7a..6f058e023 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -4,6 +4,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { ProjectMembershipRole, + ProjectType, ProjectUpgradeStatus, SecretEncryptionAlgo, SecretKeyEncoding, @@ -186,13 +187,15 @@ export const secretServiceFactory = ({ projectId, ...inputSecret }: TCreateSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -301,13 +304,15 @@ export const secretServiceFactory = ({ projectId, ...inputSecret }: TUpdateSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -443,13 +448,15 @@ export const secretServiceFactory = ({ projectId, ...inputSecret }: TDeleteSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -732,13 +739,14 @@ export const secretServiceFactory = ({ projectId, secrets: inputSecrets }: TCreateBulkSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -817,13 +825,15 @@ export const secretServiceFactory = ({ projectId, secrets: inputSecrets }: TUpdateBulkSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) @@ -923,13 +933,14 @@ export const secretServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteBulkSecretDTO) => { - const { permission } = await permissionService.getProjectPermission( + const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); + ForbidOnInvalidProjectType(ProjectType.SecretManager); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index bdf2fe18c..a2ed85749 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -53,6 +53,13 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => { const smtp = createTransport(cfg); const isSmtpOn = Boolean(cfg.host); + handlebars.registerHelper("emailFooter", () => { + const { SITE_URL } = getConfig(); + return new handlebars.SafeString( + `

Email sent via Infisical at ${SITE_URL}

` + ); + }); + const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => { const appCfg = getConfig(); const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8"); diff --git a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars index 82c66ce5f..3c0811a1c 100644 --- a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars +++ b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars @@ -45,6 +45,8 @@ View the request and approve or deny it here.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars b/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars index 3313d352f..8c82df289 100644 --- a/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars +++ b/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars @@ -11,8 +11,11 @@

A secret approval request has been bypassed in the project "{{projectName}}".

- {{requesterFullName}} ({{requesterEmail}}) has merged - a secret to environment {{environment}} at secret path {{secretPath}} + {{requesterFullName}} + ({{requesterEmail}}) has merged a secret to environment + {{environment}} + at secret path + {{secretPath}} without obtaining the required approvals.

@@ -24,5 +27,7 @@ To review this action, please visit the request panel here.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/emailMfa.handlebars b/backend/src/services/smtp/templates/emailMfa.handlebars index 936195c34..4c948b08c 100644 --- a/backend/src/services/smtp/templates/emailMfa.handlebars +++ b/backend/src/services/smtp/templates/emailMfa.handlebars @@ -1,4 +1,3 @@ - @@ -14,6 +13,8 @@

{{code}}

The MFA code will be valid for 2 minutes.

Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/emailVerification.handlebars b/backend/src/services/smtp/templates/emailVerification.handlebars index ad9694d5c..4a989626e 100644 --- a/backend/src/services/smtp/templates/emailVerification.handlebars +++ b/backend/src/services/smtp/templates/emailVerification.handlebars @@ -10,6 +10,8 @@

Confirm your email address

Your confirmation code is below — enter it in the browser window where you've started confirming your email.

{{code}}

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/externalImportFailed.handlebars b/backend/src/services/smtp/templates/externalImportFailed.handlebars index c7869af27..1755052c1 100644 --- a/backend/src/services/smtp/templates/externalImportFailed.handlebars +++ b/backend/src/services/smtp/templates/externalImportFailed.handlebars @@ -16,6 +16,7 @@

Error: {{error}}

+ {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/externalImportStarted.handlebars b/backend/src/services/smtp/templates/externalImportStarted.handlebars index 551f972cc..90026f762 100644 --- a/backend/src/services/smtp/templates/externalImportStarted.handlebars +++ b/backend/src/services/smtp/templates/externalImportStarted.handlebars @@ -12,6 +12,8 @@ {{provider}} to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import has finished or if it fails.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/externalImportSuccessful.handlebars b/backend/src/services/smtp/templates/externalImportSuccessful.handlebars index 51a1c465e..a918e9ec7 100644 --- a/backend/src/services/smtp/templates/externalImportSuccessful.handlebars +++ b/backend/src/services/smtp/templates/externalImportSuccessful.handlebars @@ -9,6 +9,8 @@

An import from {{provider}} to Infisical was successful

An import from {{provider}} was successful. Your data is now available in Infisical.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars b/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars index 0798538fb..4a918ee0d 100644 --- a/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars +++ b/backend/src/services/smtp/templates/historicalSecretLeakIncident.handlebars @@ -1,21 +1,21 @@ - - - - - Incident alert: secrets potentially leaked - + + + + Incident alert: secrets potentially leaked + - -

Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo

-

View leaked secrets

+ +

Infisical has uncovered {{numberOfSecrets}} secret(s) from historical commits to your repo

+

View leaked secrets

-

If these are production secrets, please rotate them immediately.

+

If these are production secrets, please rotate them immediately.

-

Once you have taken action, be sure to update the status of the risk in your Infisical - dashboard.

- +

Once you have taken action, be sure to update the status of the risk in your + Infisical dashboard.

+ + {{emailFooter}} + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/integrationSyncFailed.handlebars b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars index 5c5d76693..2aff820fa 100644 --- a/backend/src/services/smtp/templates/integrationSyncFailed.handlebars +++ b/backend/src/services/smtp/templates/integrationSyncFailed.handlebars @@ -26,6 +26,8 @@ {{#if syncMessage}}

Reason: {{syncMessage}}

{{/if}} + + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/newDevice.handlebars b/backend/src/services/smtp/templates/newDevice.handlebars index 6c7f2e9f6..197e0b7a7 100644 --- a/backend/src/services/smtp/templates/newDevice.handlebars +++ b/backend/src/services/smtp/templates/newDevice.handlebars @@ -1,4 +1,3 @@ - @@ -13,7 +12,11 @@

Timestamp: {{timestamp}}

IP address: {{ip}}

User agent: {{userAgent}}

-

If you believe that this login is suspicious, please contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} or reset your password immediately.

+

If you believe that this login is suspicious, please contact + {{#if isCloud}}Infisical{{else}}your administrator{{/if}} + or reset your password immediately.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/organizationInvitation.handlebars b/backend/src/services/smtp/templates/organizationInvitation.handlebars index c3ac9556d..da429477b 100644 --- a/backend/src/services/smtp/templates/organizationInvitation.handlebars +++ b/backend/src/services/smtp/templates/organizationInvitation.handlebars @@ -12,5 +12,7 @@ Click to join

What is Infisical?

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

+ + {{emailFooter}} diff --git a/backend/src/services/smtp/templates/passwordReset.handlebars b/backend/src/services/smtp/templates/passwordReset.handlebars index 6499a629c..1cb2ae8ce 100644 --- a/backend/src/services/smtp/templates/passwordReset.handlebars +++ b/backend/src/services/smtp/templates/passwordReset.handlebars @@ -1,14 +1,16 @@ - - - - + + + Account Recovery - - + +

Reset your password

Someone requested a password reset.

Reset password -

If you didn't initiate this request, please contact {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}

- +

If you didn't initiate this request, please contact + {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}

+ + {{emailFooter}} + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars b/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars index 77d2543ae..f9013e24d 100644 --- a/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars +++ b/backend/src/services/smtp/templates/pkiExpirationAlert.handlebars @@ -27,5 +27,7 @@

Please take necessary actions to renew these items before they expire.

For more details, please log in to your Infisical account and check your PKI management section.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/scimUserProvisioned.handlebars b/backend/src/services/smtp/templates/scimUserProvisioned.handlebars index b1482aa17..ba04d7201 100644 --- a/backend/src/services/smtp/templates/scimUserProvisioned.handlebars +++ b/backend/src/services/smtp/templates/scimUserProvisioned.handlebars @@ -1,16 +1,18 @@ - - - - + + + Organization Invitation - - + +

Join your organization on Infisical

You've been invited to join the Infisical organization — {{organizationName}}

Join now

What is Infisical?

-

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

- +

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets + and configs.

+ + {{emailFooter}} + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars b/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars index 9dd6fe747..c12c08460 100644 --- a/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars +++ b/backend/src/services/smtp/templates/secretApprovalRequestNeedsReview.handlebars @@ -17,6 +17,8 @@ View the request and approve or deny it here.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretLeakIncident.handlebars b/backend/src/services/smtp/templates/secretLeakIncident.handlebars index c3c5f353a..d0d9a617c 100644 --- a/backend/src/services/smtp/templates/secretLeakIncident.handlebars +++ b/backend/src/services/smtp/templates/secretLeakIncident.handlebars @@ -1,25 +1,27 @@ - - - - - Incident alert: secret leaked - + + + + Incident alert: secret leaked + - -

Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push

-

View leaked secrets

-

You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed - by {{pusher_name}} ({{pusher_email}}). If - these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment - in the given programming. This will prevent future notifications from being sent out for those secret(s).

+ +

Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push

+

View leaked secrets

+

You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed + by + {{pusher_name}} + ({{pusher_email}}). If these are test secrets, please add `infisical-scan:ignore` at the end of the line + containing the secret as comment in the given programming. This will prevent future notifications from being sent + out for those secret(s).

-

If these are production secrets, please rotate them immediately.

+

If these are production secrets, please rotate them immediately.

-

Once you have taken action, be sure to update the status of the risk in your Infisical - dashboard.

- +

Once you have taken action, be sure to update the status of the risk in your + Infisical dashboard.

+ + {{emailFooter}} + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/secretReminder.handlebars b/backend/src/services/smtp/templates/secretReminder.handlebars index 2a0efcac8..d64c4bf42 100644 --- a/backend/src/services/smtp/templates/secretReminder.handlebars +++ b/backend/src/services/smtp/templates/secretReminder.handlebars @@ -13,6 +13,8 @@ {{#if reminderNote}}

Here's the note included with the reminder: {{reminderNote}}

{{/if}} + + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/signupEmailVerification.handlebars b/backend/src/services/smtp/templates/signupEmailVerification.handlebars index 3ba18619f..39f47ae48 100644 --- a/backend/src/services/smtp/templates/signupEmailVerification.handlebars +++ b/backend/src/services/smtp/templates/signupEmailVerification.handlebars @@ -1,17 +1,19 @@ - - - - + + + Code - + - +

Confirm your email address

Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.

{{code}}

-

Questions about setting up Infisical? {{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.

- +

Questions about setting up Infisical? + {{#if isCloud}}Email us at support@infisical.com{{else}}Contact your administrator{{/if}}.

+ + {{emailFooter}} + \ No newline at end of file diff --git a/backend/src/services/smtp/templates/unlockAccount.handlebars b/backend/src/services/smtp/templates/unlockAccount.handlebars index 36664be87..b65cb5625 100644 --- a/backend/src/services/smtp/templates/unlockAccount.handlebars +++ b/backend/src/services/smtp/templates/unlockAccount.handlebars @@ -11,6 +11,8 @@

Your account has been temporarily locked due to multiple failed login attempts. To unlock your account, follow the link here

If these attempts were not made by you, reset your password immediately.

+ + {{emailFooter}} \ No newline at end of file diff --git a/backend/src/services/smtp/templates/workspaceInvitation.handlebars b/backend/src/services/smtp/templates/workspaceInvitation.handlebars index b82b8b2c2..fde75a6d6 100644 --- a/backend/src/services/smtp/templates/workspaceInvitation.handlebars +++ b/backend/src/services/smtp/templates/workspaceInvitation.handlebars @@ -11,5 +11,7 @@

What is Infisical?

Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.

+ + {{emailFooter}} diff --git a/backend/src/services/user-engagement/user-engagement-service.ts b/backend/src/services/user-engagement/user-engagement-service.ts index 5d7b54929..b14672903 100644 --- a/backend/src/services/user-engagement/user-engagement-service.ts +++ b/backend/src/services/user-engagement/user-engagement-service.ts @@ -1,87 +1,44 @@ -import { PlainClient } from "@team-plain/typescript-sdk"; +import axios from "axios"; import { getConfig } from "@app/lib/config/env"; import { InternalServerError } from "@app/lib/errors"; +import { TOrgDALFactory } from "../org/org-dal"; import { TUserDALFactory } from "../user/user-dal"; type TUserEngagementServiceFactoryDep = { userDAL: Pick; + orgDAL: Pick; }; export type TUserEngagementServiceFactory = ReturnType; -export const userEngagementServiceFactory = ({ userDAL }: TUserEngagementServiceFactoryDep) => { - const createUserWish = async (userId: string, text: string) => { +export const userEngagementServiceFactory = ({ userDAL, orgDAL }: TUserEngagementServiceFactoryDep) => { + const createUserWish = async (userId: string, orgId: string, text: string) => { const user = await userDAL.findById(userId); + const org = await orgDAL.findById(orgId); const appCfg = getConfig(); - if (!appCfg.PLAIN_API_KEY) { + if (!appCfg.PYLON_API_KEY) { throw new InternalServerError({ - message: "Plain is not configured." + message: "Pylon is not configured." }); } - const client = new PlainClient({ - apiKey: appCfg.PLAIN_API_KEY - }); - - const customerUpsertRes = await client.upsertCustomer({ - identifier: { - emailAddress: user.email - }, - onCreate: { - fullName: `${user.firstName} ${user.lastName}`, - shortName: user.firstName, - email: { - email: user.email as string, - isVerified: user.isEmailVerified as boolean - }, - - externalId: user.id - }, - - onUpdate: { - fullName: { - value: `${user.firstName} ${user.lastName}` - }, - shortName: { - value: user.firstName - }, - email: { - email: user.email as string, - isVerified: user.isEmailVerified as boolean - }, - externalId: { - value: user.id - } + const request = axios.create({ + baseURL: "https://api.usepylon.com", + headers: { + Authorization: `Bearer ${appCfg.PYLON_API_KEY}` } }); - if (customerUpsertRes.error) { - throw new InternalServerError({ message: customerUpsertRes.error.message }); - } - - const createThreadRes = await client.createThread({ - title: "Wish", - customerIdentifier: { - externalId: customerUpsertRes.data.customer.externalId - }, - components: [ - { - componentText: { - text - } - } - ], - labelTypeIds: appCfg.PLAIN_WISH_LABEL_IDS?.split(",") + await request.post("/issues", { + title: `New Wish From: ${user.firstName} ${user.lastName} (${org.name})`, + body_html: text, + requester_email: user.email, + requester_name: `${user.firstName} ${user.lastName} (${org.name})`, + tags: ["wish"] }); - - if (createThreadRes.error) { - throw new InternalServerError({ - message: createThreadRes.error.message - }); - } }; return { createUserWish diff --git a/docs/api-reference/endpoints/jwt-auth/attach.mdx b/docs/api-reference/endpoints/jwt-auth/attach.mdx new file mode 100644 index 000000000..f2905f1a0 --- /dev/null +++ b/docs/api-reference/endpoints/jwt-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/jwt-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/jwt-auth/login.mdx b/docs/api-reference/endpoints/jwt-auth/login.mdx new file mode 100644 index 000000000..c037fbf7f --- /dev/null +++ b/docs/api-reference/endpoints/jwt-auth/login.mdx @@ -0,0 +1,4 @@ +--- +title: "Login" +openapi: "POST /api/v1/auth/jwt-auth/login" +--- diff --git a/docs/api-reference/endpoints/jwt-auth/retrieve.mdx b/docs/api-reference/endpoints/jwt-auth/retrieve.mdx new file mode 100644 index 000000000..8100ef843 --- /dev/null +++ b/docs/api-reference/endpoints/jwt-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/jwt-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/jwt-auth/revoke.mdx b/docs/api-reference/endpoints/jwt-auth/revoke.mdx new file mode 100644 index 000000000..13a61475a --- /dev/null +++ b/docs/api-reference/endpoints/jwt-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/jwt-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/jwt-auth/update.mdx b/docs/api-reference/endpoints/jwt-auth/update.mdx new file mode 100644 index 000000000..8a53907ab --- /dev/null +++ b/docs/api-reference/endpoints/jwt-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/jwt-auth/identities/{identityId}" +--- diff --git a/docs/documentation/platform/identities/jwt-auth.mdx b/docs/documentation/platform/identities/jwt-auth.mdx new file mode 100644 index 000000000..3dcf12b29 --- /dev/null +++ b/docs/documentation/platform/identities/jwt-auth.mdx @@ -0,0 +1,169 @@ +--- +title: JWT Auth +description: "Learn how to authenticate with Infisical using JWT-based authentication." +--- + +**JWT Auth** is a platform-agnostic authentication method that validates JSON Web Tokens (JWTs) issued by your JWT issuer or authentication system, allowing secure authentication from any platform or environment that can obtain valid JWTs. + +## Diagram + +The following sequence diagram illustrates the JWT Auth workflow for authenticating with Infisical. + +```mermaid +sequenceDiagram + participant Client as Client Application + participant Issuer as JWT Issuer + participant Infis as Infisical + + Client->>Issuer: Step 1: Request JWT token + Issuer-->>Client: Return signed JWT with claims + + Note over Client,Infis: Step 2: Login Operation + Client->>Infis: Send signed JWT to /api/v1/auth/jwt-auth/login + + Note over Infis: Step 3: JWT Validation + Infis->>Infis: Validate JWT signature using configured public keys or JWKS + Infis->>Infis: Verify required claims (aud, sub, iss) + + Note over Infis: Step 4: Token Generation + Infis->>Client: Return short-lived access token + + Note over Client,Infis: Step 5: Access Infisical API with Token + Client->>Infis: Make authenticated requests using the short-lived access token +``` + +## Concept + +At a high-level, Infisical authenticates a client by verifying the JWT and checking that it meets specific requirements (e.g. it is signed by a trusted key) at the `/api/v1/auth/jwt-auth/login` endpoint. If successful, then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. + +To be more specific: + +1. The client requests a JWT from their JWT issuer. +2. The fetched JWT is sent to Infisical at the `/api/v1/auth/jwt-auth/login` endpoint. +3. Infisical validates the JWT signature using either: + - Pre-configured public keys (Static configuration) + - Public keys fetched from a JWKS endpoint (JWKS configuration) +4. Infisical verifies that the configured claims match in the token. This includes standard claims like subject, audience, and issuer, as well as any additional custom claims specified in the configuration. +5. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API. + + + For JWKS configuration, Infisical needs network-level access to the configured + JWKS endpoint. + + +## Guide + +In the following steps, we explore how to create and use identities to access the Infisical API using the JWT authentication method. + + + + To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + + ![identities organization](/images/platform/identities/identities-org.png) + + When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. + + ![identities organization create](/images/platform/identities/identities-org-create.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. + + ![identities page](/images/platform/identities/identities-page.png) + + Since the identity has been configured with Universal Auth by default, you should re-configure it to use JWT Auth instead. To do this, press to edit the **Authentication** section, + remove the existing Universal Auth configuration, and add a new JWT Auth configuration onto the identity. + + ![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png) + + ![identities create jwt auth method](/images/platform/identities/identities-org-create-jwt-auth-method-jwks.png) + ![identities create jwt auth method](/images/platform/identities/identities-org-create-jwt-auth-method-static.png) + + Restrict access by properly configuring the JWT validation settings. + + Here's some more guidance for each field: + + **Static configuration**: + - Public Keys: One or more PEM-encoded public keys (RSA or ECDSA) used to verify JWT signatures. Each key must include the proper BEGIN/END markers. + + **JWKS configuration**: + - JWKS URL: The endpoint URL that serves your JSON Web Key Sets (JWKS). This endpoint must provide the public keys used for JWT signature verification. + - JWKS CA Certificate: Optional PEM-encoded CA certificate used for validating the TLS connection to the JWKS endpoint. + + **Common fields for both configurations**: + - Issuer: The unique identifier of the JWT provider. This value is used to verify the iss (issuer) claim in the JWT. + - Audiences: A list of intended recipients. This value is checked against the aud (audience) claim in the token. + - Subject: The expected principal that is the subject of the JWT. This value is checked against the sub (subject) claim in the token. + - Claims: Additional claims that must be present in the JWT for it to be valid. You can specify required claim names and their expected values. + - 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. + + The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible. + + + + To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. + + To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. + + Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. + + ![identities project](/images/platform/identities/identities-project.png) + + ![identities project create](/images/platform/identities/identities-project-create.png) + + + + To access the Infisical API as the identity, you will need to obtain a JWT from your JWT issuer that meets the validation requirements configured in step 2. + + Once you have obtained a valid JWT, you can use it to authenticate with Infisical at the `/api/v1/auth/jwt-auth/login` endpoint. + + We provide a code example below of how you might use the JWT to authenticate with Infisical to gain access to the [Infisical API](/api-reference/overview/introduction). + + + The shown example uses Node.js but you can use any other language to authenticate with Infisical using your JWT. + + ```javascript + try { + // Obtain JWT from your issuer + const jwt = ""; + + const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL + const identityId = ""; + + const { data } = await axios.post( + `{infisicalUrl}/api/v1/auth/jwt-auth/login`, + { + identityId, + jwt, + } + ); + + console.log("result data: ", data); // access token here + } catch(err) { + console.error(err); + } + ``` + + + + We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using JWT Auth as they handle the authentication process for you. + + + + Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation; + the default TTL is `2592000` seconds (30 days) which can be adjusted in the configuration. + + If an identity access token exceeds its max TTL or maximum number of uses, it can no longer authenticate with the Infisical API. In this case, + a new access token should be obtained by performing another login operation with a valid JWT. + + + + diff --git a/docs/images/platform/identities/identities-org-create-jwt-auth-method-jwks.png b/docs/images/platform/identities/identities-org-create-jwt-auth-method-jwks.png new file mode 100644 index 000000000..1f693b346 Binary files /dev/null and b/docs/images/platform/identities/identities-org-create-jwt-auth-method-jwks.png differ diff --git a/docs/images/platform/identities/identities-org-create-jwt-auth-method-static.png b/docs/images/platform/identities/identities-org-create-jwt-auth-method-static.png new file mode 100644 index 000000000..5d434a6e0 Binary files /dev/null and b/docs/images/platform/identities/identities-org-create-jwt-auth-method-static.png differ diff --git a/docs/integrations/cloud/azure-devops.mdx b/docs/integrations/cloud/azure-devops.mdx index 6d1ba6b17..4eaaf0cc1 100644 --- a/docs/integrations/cloud/azure-devops.mdx +++ b/docs/integrations/cloud/azure-devops.mdx @@ -21,7 +21,7 @@ You'll need to create a new personal access token (PAT) in order to authenticate ![integrations](../../images/integrations/azure-devops/create-new-token.png) - Please make sure that the token has access to the following scopes: Variable Groups _(read/write)_, Release _(read/write)_, Project and Team _(read)_, Service Connections _(read & query)_ + Please make sure that the token has access to the following scopes: Variable Groups _(read, create, & manage)_, Release _(read/write)_, Project and Team _(read)_, Service Connections _(read & query)_ diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index a925f6c4b..6defea119 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -41,6 +41,30 @@ The operator can be install via [Helm](https://helm.sh) or [kubectl](https://git helm install --generate-name infisical-helm-charts/secrets-operator --version=0.1.4 --set controllerManager.manager.image.tag=v0.2.0 ``` + **Namespace-scoped Installation** + + The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access. + + ```bash + helm install operator infisical-helm-charts/secrets-operator \ + --namespace your-namespace \ + --set scopedNamespace=your-namespace \ + --set scopedRBAC=true + ``` + + When scoped to a namespace, the operator will: + + - Only watch InfisicalSecrets in the specified namespace + - Only create/update Kubernetes secrets in that namespace + - Only access deployments in that namespace + + The default configuration gives cluster-wide access: + + ```yaml + scopedNamespace: "" # Empty for cluster-wide access + scopedRBAC: false # Cluster-wide permissions + ``` + For production deployments, it is highly recommended to set the version of the Kubernetes operator manually instead of pointing to the latest version. diff --git a/docs/mint.json b/docs/mint.json index 9c28137fa..030a0b4dc 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -32,10 +32,7 @@ "thumbsRating": true }, "api": { - "baseUrl": [ - "https://app.infisical.com", - "http://localhost:8080" - ] + "baseUrl": ["https://app.infisical.com", "http://localhost:8080"] }, "topbarLinks": [ { @@ -76,9 +73,7 @@ "documentation/getting-started/introduction", { "group": "Quickstart", - "pages": [ - "documentation/guides/local-development" - ] + "pages": ["documentation/guides/local-development"] }, { "group": "Guides", @@ -229,6 +224,7 @@ "documentation/platform/identities/gcp-auth", "documentation/platform/identities/azure-auth", "documentation/platform/identities/aws-auth", + "documentation/platform/identities/jwt-auth", { "group": "OIDC Auth", "pages": [ @@ -467,15 +463,11 @@ }, { "group": "Build Tool Integrations", - "pages": [ - "integrations/build-tools/gradle" - ] + "pages": ["integrations/build-tools/gradle"] }, { "group": "", - "pages": [ - "sdks/overview" - ] + "pages": ["sdks/overview"] }, { "group": "SDK's", @@ -495,9 +487,7 @@ "api-reference/overview/authentication", { "group": "Examples", - "pages": [ - "api-reference/overview/examples/integration" - ] + "pages": ["api-reference/overview/examples/integration"] } ] }, @@ -593,6 +583,16 @@ "api-reference/endpoints/oidc-auth/revoke" ] }, + { + "group": "JWT Auth", + "pages": [ + "api-reference/endpoints/jwt-auth/login", + "api-reference/endpoints/jwt-auth/attach", + "api-reference/endpoints/jwt-auth/retrieve", + "api-reference/endpoints/jwt-auth/update", + "api-reference/endpoints/jwt-auth/revoke" + ] + }, { "group": "Groups", "pages": [ @@ -772,15 +772,11 @@ }, { "group": "Service Tokens", - "pages": [ - "api-reference/endpoints/service-tokens/get" - ] + "pages": ["api-reference/endpoints/service-tokens/get"] }, { "group": "Audit Logs", - "pages": [ - "api-reference/endpoints/audit-logs/export-audit-log" - ] + "pages": ["api-reference/endpoints/audit-logs/export-audit-log"] } ] }, @@ -879,9 +875,7 @@ }, { "group": "", - "pages": [ - "changelog/overview" - ] + "pages": ["changelog/overview"] }, { "group": "Contributing", @@ -905,9 +899,7 @@ }, { "group": "Contributing to SDK", - "pages": [ - "contributing/sdk/developing" - ] + "pages": ["contributing/sdk/developing"] } ] } @@ -1090,4 +1082,4 @@ } ] } -} \ No newline at end of file +} diff --git a/frontend/public/lotties/note.json b/frontend/public/lotties/note.json new file mode 100644 index 000000000..783cab705 --- /dev/null +++ b/frontend/public/lotties/note.json @@ -0,0 +1,808 @@ +{ + "v": "5.12.2", + "fr": 29.9700012207031, + "ip": 0, + "op": 45.0000018328876, + "w": 48, + "h": 48, + "nm": "note", + "ddd": 1, + "assets": [], + "layers": [ + { + "ddd": 1, + "ind": 1, + "ty": 4, + "nm": "note-outline-bot_s1g1_s2g2_s3g1_s4g1 Outlines", + "parent": 2, + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "rx": { "a": 0, "k": 0, "ix": 8 }, + "ry": { "a": 0, "k": 0, "ix": 9 }, + "rz": { + "a": 1, + "k": [ + { "i": { "x": [0], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 0, "s": [0] }, + { "i": { "x": [0], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 22, "s": [5] }, + { "t": 44.0000017921567, "s": [0] } + ], + "ix": 10 + }, + "or": { "a": 0, "k": [0, 0, 0], "ix": 7 }, + "p": { "a": 0, "k": [19.448, 27.122, 0], "ix": 2 }, + "a": { "a": 0, "k": [13.405, 11.539, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 18.078], + [21.809, 18.078] + ], + "c": false + } + ] + }, + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 22, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [4.311, 17.73], + [21.654, 16.837] + ], + "c": false + } + ] + }, + { + "t": 44.0000017921567, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 18.078], + [21.809, 18.078] + ], + "c": false + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 1, + "ty": "sh", + "ix": 2, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 11.281], + [21.809, 11.281] + ], + "c": false + } + ] + }, + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 22, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 11.281], + [21.735, 9.907] + ], + "c": false + } + ] + }, + { + "t": 44.0000017921567, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 11.281], + [21.809, 11.281] + ], + "c": false + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 2", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 2, + "ty": "sh", + "ix": 3, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 5], + [13.756, 5] + ], + "c": false + } + ] + }, + { + "i": { "x": 0.022, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 22, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5.497, 4.939], + [14.633, 4.166] + ], + "c": false + } + ] + }, + { + "t": 44.0000017921567, + "s": [ + { + "i": [ + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0] + ], + "v": [ + [5, 5], + [13.756, 5] + ], + "c": false + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 3", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "mm", + "mm": 1, + "nm": "Merge Paths 1", + "mn": "ADBE Vector Filter - Merge", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 5, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 1 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 3 }, + "m": 1, + "ix": 2, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + }, + { + "ty": "tm", + "s": { "a": 0, "k": 0, "ix": 1 }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { "a": 0, "k": 0, "ix": 3 }, + "m": 1, + "ix": 3, + "nm": "Trim Paths 2", + "mn": "ADBE Vector Filter - Trim", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.616], "y": [1] }, + "o": { "x": [0.41], "y": [0] }, + "t": 0, + "s": [0] + }, + { + "i": { "x": [0.005], "y": [1] }, + "o": { "x": [0.369], "y": [0] }, + "t": 22, + "s": [100] + }, + { "t": 44.0000017921567, "s": [0] } + ], + "ix": 1 + }, + "e": { "a": 0, "k": 100, "ix": 2 }, + "o": { + "a": 1, + "k": [ + { + "i": { "x": [0], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 22, + "s": [0] + }, + { "t": 44.0000017921567, "s": [360] } + ], + "ix": 3 + }, + "m": 1, + "ix": 1, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 4, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 45.0000018328876, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 1, + "ind": 2, + "ty": 4, + "nm": "note-outline-bot_s1g1_s2g1_s3g1_s4g1_background Outlines", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "rx": { "a": 0, "k": 0, "ix": 8 }, + "ry": { "a": 0, "k": 0, "ix": 9 }, + "rz": { "a": 0, "k": 0, "ix": 10 }, + "or": { "a": 0, "k": [0, 0, 0], "ix": 7 }, + "p": { "a": 0, "k": [24.778, 23.858, 0], "ix": 2 }, + "a": { "a": 0, "k": [19.825, 23.313, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [1.423, 1.657], + [0, 0], + [0, 0], + [0, -2.354], + [0, 0], + [-1.324, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [-1.482, -1.727], + [0, 0], + [0, 1.429], + [0, 0], + [2.093, 0] + ], + "v": [ + [4.581, 2.516], + [1.876, -0.634], + [-1.99, -5.137], + [-6.005, -3.212], + [-6.005, 4.277], + [-3.606, 6.865], + [2.822, 6.865] + ], + "c": true + } + ] + }, + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 22, + "s": [ + { + "i": [ + [0.56, 0.711], + [0.835, 1.361], + [0, 0], + [-0.031, 0.137], + [0, 0], + [-0.118, -0.836], + [0, 0] + ], + "o": [ + [0, 0], + [-0.835, -1.361], + [-0.299, -0.286], + [0, 0], + [1.062, 0.814], + [0, 0], + [-0.101, 0.125] + ], + "v": [ + [6.281, -2.819], + [5.393, -4.55], + [3.663, -5.916], + [3.337, -6.047], + [4.119, -5.661], + [6.298, -2.792], + [6.5, -1.722] + ], + "c": true + } + ] + }, + { + "t": 44.0000017921567, + "s": [ + { + "i": [ + [1.423, 1.657], + [0, 0], + [0, 0], + [0, -2.354], + [0, 0], + [-1.324, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [-1.482, -1.727], + [0, 0], + [0, 1.429], + [0, 0], + [2.093, 0] + ], + "v": [ + [4.581, 2.516], + [1.876, -0.634], + [-1.99, -5.137], + [-6.005, -3.212], + [-6.005, 4.277], + [-3.606, 6.865], + [2.822, 6.865] + ], + "c": true + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 4 }, + "o": { "a": 0, "k": 0, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [28.646, 11.865], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [0, -0.781], + [0, 0], + [2.763, 0], + [0, 0], + [0, 2.828], + [0, 0], + [0, 0], + [-2.763, 0], + [0, 0], + [-0.625, -0.733] + ], + "o": [ + [0.507, 0.595], + [0, 0], + [0, 2.828], + [0, 0], + [-2.763, 0], + [0, 0], + [0, 0], + [0, -2.828], + [0, 0], + [0.963, 0], + [0, 0] + ], + "v": [ + [14.012, -8.411], + [14.797, -6.279], + [14.797, 13.05], + [9.794, 18.171], + [-9.794, 18.171], + [-14.796, 13.05], + [-14.796, 0.99], + [-14.796, -13.051], + [-9.794, -18.171], + [4.174, -18.171], + [6.676, -17.016] + ], + "c": true + } + ] + }, + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 22, + "s": [ + { + "i": [ + [0, 0], + [0.171, -6.028], + [0, 0], + [2.763, 0], + [0, 0], + [0, 2.828], + [-1.453, 6.385], + [0, 0], + [-2.763, 0], + [0, 0], + [-1.739, -0.297] + ], + "o": [ + [0.051, 2.426], + [-0.171, 6.028], + [-1.046, 2.979], + [0, 0], + [-2.763, 0], + [0, 0], + [1.266, -5.562], + [0, -2.828], + [0, 0], + [0.963, 0], + [0, 0] + ], + "v": [ + [15.322, -13.817], + [14.796, -0.028], + [12.547, 13.021], + [6.669, 18.201], + [-12.044, 18.141], + [-17.046, 13.021], + [-14.421, 1.025], + [-12.923, -12.566], + [-7.92, -17.686], + [3.673, -17.672], + [12.612, -17.453] + ], + "c": true + } + ] + }, + { + "t": 44.0000017921567, + "s": [ + { + "i": [ + [0, 0], + [0, -0.781], + [0, 0], + [2.763, 0], + [0, 0], + [0, 2.828], + [0, 0], + [0, 0], + [-2.763, 0], + [0, 0], + [-0.625, -0.733] + ], + "o": [ + [0.507, 0.595], + [0, 0], + [0, 2.828], + [0, 0], + [-2.763, 0], + [0, 0], + [0, 0], + [0, -2.828], + [0, 0], + [0.963, 0], + [0, 0] + ], + "v": [ + [14.012, -8.411], + [14.797, -6.279], + [14.797, 13.05], + [9.794, 18.171], + [-9.794, 18.171], + [-14.796, 13.05], + [-14.796, 0.99], + [-14.796, -13.051], + [-9.794, -18.171], + [4.174, -18.171], + [6.676, -17.016] + ], + "c": true + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 4 }, + "o": { "a": 0, "k": 0, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [19.797, 23.455], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 45.0000018328876, + "st": 0, + "ct": 1, + "bm": 0 + } + ], + "markers": [], + "props": {} +} diff --git a/frontend/public/lotties/system-regular-126-verified-hover-verified.json b/frontend/public/lotties/system-regular-126-verified-hover-verified.json new file mode 100644 index 000000000..ce8777c0a --- /dev/null +++ b/frontend/public/lotties/system-regular-126-verified-hover-verified.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":500,"h":500,"nm":"system-regular-126-verified","ddd":0,"assets":[{"id":"comp_1","nm":"hover-verified","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[6.109,-6.116],[-6.115,-6.108],[0,0],[-4.002,0],[-3.055,3.051],[0,0],[6.107,6.115],[6.115,-6.107],[0,0]],"o":[[-6.115,-6.107],[-6.107,6.116],[0,0],[3.056,3.052],[4.002,0],[0,0],[6.115,-6.108],[-6.109,-6.116],[0,0],[0,0]],"v":[[-69.803,-8.539],[-91.936,-8.526],[-91.922,13.607],[-39.704,65.762],[-28.644,70.339],[-17.584,65.762],[91.922,-43.616],[91.936,-65.749],[69.803,-65.762],[-28.644,32.57]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[263.158,242.187],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[73.521,-47.165],[0,0],[0,0],[11.506,86.895],[0,0],[-39.84,20.453],[-33.146,-7.159]],"o":[[-11.51,86.919],[0,0],[0,0],[-73.503,-47.153],[0,0],[33.146,-7.159],[39.84,20.453],[0,0]],"v":[[149.277,-67.68],[15.49,143.298],[0,153.266],[-15.509,143.287],[-149.274,-67.655],[-154.479,-107.449],[0,-153.936],[154.479,-107.449]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.073,1.615],[32.173,20.107],[5.074,-3.172],[35.445,-7.092],[-1.068,-8.164],[0,0],[-81.252,-52.124],[0,0],[-2.944,0],[-2.579,1.66],[0,0],[-12.724,96.093],[0,0]],"o":[[-35.445,-7.092],[-5.074,-3.172],[-32.173,20.107],[-8.073,1.615],[0,0],[12.72,96.068],[0,0],[2.579,1.66],[2.944,0],[0,0],[81.27,-52.136],[0,0],[1.068,-8.164]],"v":[[174.946,-135.138],[8.294,-185.147],[-8.294,-185.147],[-174.946,-135.138],[-187.394,-117.763],[-180.307,-63.572],[-32.428,169.62],[-8.469,185.036],[0,187.526],[8.469,185.036],[32.409,169.632],[180.31,-63.596],[187.394,-117.763]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[6.109,-6.116],[-6.115,-6.108],[0,0],[-4.002,0],[-3.055,3.051],[0,0],[6.107,6.115],[6.115,-6.107],[0,0]],"o":[[-6.115,-6.107],[-6.107,6.116],[0,0],[3.056,3.052],[4.002,0],[0,0],[6.115,-6.108],[-6.109,-6.116],[0,0],[0,0]],"v":[[-69.803,-8.539],[-91.936,-8.526],[-91.922,13.607],[-39.704,65.762],[-28.644,70.339],[-17.584,65.762],[91.922,-43.616],[91.936,-65.749],[69.803,-65.762],[-28.644,32.57]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[263.158,242.187],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[73.521,-47.165],[0,0],[0,0],[11.506,86.895],[0,0],[-39.84,20.453],[-33.146,-7.159]],"o":[[-11.51,86.919],[0,0],[0,0],[-73.503,-47.153],[0,0],[33.146,-7.159],[39.84,20.453],[0,0]],"v":[[149.277,-67.68],[15.49,143.298],[0,153.266],[-15.509,143.287],[-149.274,-67.655],[-154.479,-107.449],[0,-153.936],[154.479,-107.449]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.073,1.615],[32.173,20.107],[5.074,-3.172],[35.445,-7.092],[-1.068,-8.164],[0,0],[-81.252,-52.124],[0,0],[-2.944,0],[-2.579,1.66],[0,0],[-12.724,96.093],[0,0]],"o":[[-35.445,-7.092],[-5.074,-3.172],[-32.173,20.107],[-8.073,1.615],[0,0],[12.72,96.068],[0,0],[2.579,1.66],[2.944,0],[0,0],[81.27,-52.136],[0,0],[1.068,-8.164]],"v":[[174.946,-135.138],[8.294,-185.147],[-8.294,-185.147],[-174.946,-135.138],[-187.394,-117.763],[-180.307,-63.572],[-32.428,169.62],[-8.469,185.036],[0,187.526],[8.469,185.036],[32.409,169.632],[180.31,-63.596],[187.394,-117.763]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.131],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.628],"y":[0]},"t":30,"s":[27]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":46,"s":[-11]},{"t":60,"s":[0]}],"ix":10},"p":{"a":0,"k":[249.998,250.004,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.131,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0],[77.333,-49.716],[0,0],[0,0],[0,0],[11.993,91.145],[0,0],[-41.666,26.039]],"o":[[0,0],[-11.99,91.149],[0,0],[0,0],[0,0],[-77.33,-49.714],[0,0],[0,0],[41.667,26.039]],"v":[[171.875,-119.795],[164.746,-65.6],[23.905,156.51],[0.007,171.873],[0,171.875],[-23.904,156.508],[-164.743,-65.592],[-171.875,-119.795],[0,-171.875]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.628,"y":0},"t":21,"s":[{"i":[[0,0],[0,0],[99.436,-63.925],[0,0],[0,0],[0,0],[15.421,117.196],[0,0],[-53.575,33.481]],"o":[[0,0],[-15.417,117.201],[0,0],[0,0],[0,0],[-99.432,-63.922],[0,0],[0,0],[53.576,33.481]],"v":[[191.412,-122.573],[182.245,-52.889],[1.15,232.704],[-29.579,252.458],[-29.588,252.46],[-60.324,232.701],[-241.417,-52.879],[-250.588,-122.573],[-29.588,-189.539]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":41.801,"s":[{"i":[[0,0],[0,0],[56.69,-36.445],[0,0],[0,0],[0,0],[8.792,66.816],[0,0],[-30.544,19.088]],"o":[[0,0],[-8.79,66.819],[0,0],[0,0],[0,0],[-56.688,-36.443],[0,0],[0,0],[30.545,19.088]],"v":[[125.996,-87.818],[120.77,-48.09],[17.524,114.733],[0.005,125.995],[0,125.996],[-17.523,114.731],[-120.768,-48.083],[-125.996,-87.818],[0,-125.996]],"c":true}]},{"t":60,"s":[{"i":[[0,0],[0,0],[77.333,-49.716],[0,0],[0,0],[0,0],[11.993,91.145],[0,0],[-41.666,26.039]],"o":[[0,0],[-11.99,91.149],[0,0],[0,0],[0,0],[-77.33,-49.714],[0,0],[0,0],[41.667,26.039]],"v":[[171.875,-119.795],[164.746,-65.6],[23.905,156.51],[0.007,171.873],[0,171.875],[-23.904,156.508],[-164.743,-65.592],[-171.875,-119.795],[0,-171.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":-239,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.006,-0.001,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.157,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[153.363,-119.689],[-14.644,46.689],[-66.863,-5.466]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.6,"y":0},"t":21,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[167.916,-123.188],[-50.252,92.864],[-118.061,25.138]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[114.144,-89.081],[-10.899,34.749],[-49.764,-4.068]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[153.363,-119.689],[-14.644,46.689],[-66.863,-5.466]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[100]},{"t":20,"s":[100],"h":1},{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0]},{"t":60,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[26.2]},{"t":20,"s":[100],"h":1},{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0]},{"t":60,"s":[26.2]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":60,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":131,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-verified","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":70,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-verified","dr":60}],"props":{}} \ No newline at end of file diff --git a/frontend/public/lotties/unlock.json b/frontend/public/lotties/unlock.json new file mode 100644 index 000000000..4356593e9 --- /dev/null +++ b/frontend/public/lotties/unlock.json @@ -0,0 +1,470 @@ +{ + "v": "5.12.2", + "fr": 29.9700012207031, + "ip": 0, + "op": 45.0000018328876, + "w": 48, + "h": 48, + "nm": "unlock", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "unlock-outline-top_s1g1_s2g2_s3g1_s4g1_background Outlines", + "parent": 2, + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { + "a": 1, + "k": [ + { "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 12, "s": [0] }, + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 28, + "s": [-16] + }, + { "t": 40.0000016292334, "s": [0] } + ], + "ix": 10 + }, + "p": { "a": 0, "k": [19, 19.473, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [8.5, 11.125, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 1.292], + [1.933, 0], + [0, -1.933], + [-1.042, -0.607], + [0, 0], + [-0.966, 0], + [0, 0.966], + [0, 0] + ], + "o": [ + [0, -1.933], + [-1.933, 0], + [0, 1.292], + [0, 0], + [0, 0.966], + [0.966, 0], + [0, 0], + [1.042, -0.607] + ], + "v": [ + [3.5, -2.625], + [0, -6.125], + [-3.5, -2.625], + [-1.75, 0.39], + [-1.75, 4.375], + [0, 6.125], + [1.75, 4.375], + [1.75, 0.39] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 4 }, + "o": { "a": 0, "k": 0, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [8.5, 11.125], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 45.0000018328876, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "unlock-outline-bot_s1g1_s2g1_s3g1_s4g1_background Outlines", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [24, 29.826, 0], + "to": [0, 0.313, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 12, + "s": [24, 31.701, 0], + "to": [0, 0, 0], + "ti": [0, 0.313, 0] + }, + { "t": 28.0000011404634, "s": [24, 29.826, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [19, 19.523, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [-1.43, 0.576], + [0, 0], + [-3.266, -1.28], + [0, 0], + [0, -1.555], + [0, 0], + [7.732, 0], + [0, 8.353], + [0, 0] + ], + "o": [ + [0, 0], + [3.253, -1.31], + [0, 0], + [1.448, 0.567], + [0, 0], + [0, 8.353], + [-7.732, 0], + [0, 0], + [0, -1.542] + ], + "v": [ + [-11.633, -10.628], + [-5.258, -13.195], + [4.892, -13.243], + [11.6, -10.615], + [14, -7.097], + [14, -0.602], + [0, 14.523], + [-14, -0.602], + [-14, -7.123] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [1, 1, 1], "ix": 4 }, + "o": { "a": 0, "k": 0, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [19, 19.523], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 45.0000018328876, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "unlockoutline-bot_s1g1_s2g1_s3g1_s4g2 Outlines", + "parent": 2, + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { + "a": 1, + "k": [ + { "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 0, "s": [0] }, + { "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 12, "s": [9] }, + { "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 28, "s": [9] }, + { "t": 40.0000016292334, "s": [0] } + ], + "ix": 10 + }, + "p": { "a": 0, "k": [17.628, 0.002, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [15.029, 13.585, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [5.305, -0.847], + [-0.847, -5.305], + [0, 0] + ], + "o": [ + [-0.847, -5.305], + [-5.305, 0.846], + [0, 0], + [0, 0] + ], + "v": [ + [10.029, 0.334], + [-1.11, -7.738], + [-9.182, 3.401], + [-8.355, 8.585] + ], + "c": false + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 12, + "s": [ + { + "i": [ + [0, 0], + [5.341, -0.581], + [-0.582, -5.341], + [0, 0] + ], + "o": [ + [-0.582, -5.341], + [-5.341, 0.581], + [0, 0], + [0, 0] + ], + "v": [ + [11.833, 5.021], + [1.11, -3.596], + [-7.507, 7.127], + [-7.094, 9.966] + ], + "c": false + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 28, + "s": [ + { + "i": [ + [0, 0], + [5.341, -0.581], + [-0.582, -5.341], + [0, 0] + ], + "o": [ + [-0.582, -5.341], + [-5.341, 0.581], + [0, 0], + [0, 0] + ], + "v": [ + [11.833, 5.021], + [1.11, -3.596], + [-7.507, 7.127], + [-7.094, 9.966] + ], + "c": false + } + ] + }, + { + "t": 40.0000016292334, + "s": [ + { + "i": [ + [0, 0], + [5.305, -0.847], + [-0.847, -5.305], + [0, 0] + ], + "o": [ + [-0.847, -5.305], + [-5.305, 0.846], + [0, 0], + [0, 0] + ], + "v": [ + [10.029, 0.334], + [-1.11, -7.738], + [-9.182, 3.401], + [-8.355, 8.585] + ], + "c": false + } + ] + } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2.5, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [15.029, 13.585], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 45.0000018328876, + "st": 0, + "ct": 1, + "bm": 0 + } + ], + "markers": [], + "props": {} +} diff --git a/frontend/src/components/dashboard/AddTagsMenu.tsx b/frontend/src/components/dashboard/AddTagsMenu.tsx index 3a54f77ab..d2e295c14 100644 --- a/frontend/src/components/dashboard/AddTagsMenu.tsx +++ b/frontend/src/components/dashboard/AddTagsMenu.tsx @@ -5,6 +5,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Menu, Transition } from "@headlessui/react"; import { Tag } from "public/data/frequentInterfaces"; +import { ProjectType } from "@app/hooks/api/workspace/types"; + /** * This is the menu that is used to add more tags to a secret * @param {object} obj @@ -75,7 +77,7 @@ const AddTagsMenu = ({ diff --git a/frontend/src/components/signup/TeamInviteStep.tsx b/frontend/src/components/signup/TeamInviteStep.tsx index 60276d217..63588ca5c 100644 --- a/frontend/src/components/signup/TeamInviteStep.tsx +++ b/frontend/src/components/signup/TeamInviteStep.tsx @@ -4,6 +4,7 @@ import { useRouter } from "next/router"; import { useAddUsersToOrg } from "@app/hooks/api"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { Button, EmailServiceSetupModal } from "../v2"; @@ -22,7 +23,7 @@ export default function TeamInviteStep(): JSX.Element { // Redirect user to the getting started page const redirectToHome = async () => { - router.push(`/org/${localStorage.getItem("orgData.id")}/overview`); + router.push(`/org/${localStorage.getItem("orgData.id")}/${ProjectType.SecretManager}/overview`); }; const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => { diff --git a/frontend/src/components/v2/projects/NewProjectModal.tsx b/frontend/src/components/v2/projects/NewProjectModal.tsx index 1662b07ff..3bb283617 100644 --- a/frontend/src/components/v2/projects/NewProjectModal.tsx +++ b/frontend/src/components/v2/projects/NewProjectModal.tsx @@ -32,6 +32,7 @@ import { useSubscription, useUser } from "@app/context"; +import { getProjectHomePage } from "@app/helpers/project"; import { fetchOrgUsers, useAddUserToWsNonE2EE, @@ -41,6 +42,7 @@ import { } from "@app/hooks/api"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; +import { ProjectType } from "@app/hooks/api/workspace/types"; const formSchema = z.object({ name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"), @@ -59,11 +61,12 @@ type TAddProjectFormData = z.infer; interface NewProjectModalProps { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; + projectType: ProjectType; } -type NewProjectFormProps = Pick; +type NewProjectFormProps = Pick; -const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { +const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => { const router = useRouter(); const { currentOrg } = useOrganization(); const { permission } = useOrgPermission(); @@ -117,15 +120,15 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { if (!user) return; try { const { - data: { - project: { id: newProjectId } - } + data: { project } } = await createWs.mutateAsync({ projectName: name, projectDescription: description, kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, - template + template, + type: projectType }); + const { id: newProjectId } = project; if (addMembers) { const orgUsers = await fetchOrgUsers(currentOrg.id); @@ -145,7 +148,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { createNotification({ text: "Project created", type: "success" }); reset(); onOpenChange(false); - router.push(`/project/${newProjectId}/secrets/overview`); + router.push(getProjectHomePage(project)); } catch (err) { console.error(err); createNotification({ text: "Failed to create project", type: "error" }); @@ -316,14 +319,18 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { ); }; -export const NewProjectModal: FC = ({ isOpen, onOpenChange }) => { +export const NewProjectModal: FC = ({ + isOpen, + onOpenChange, + projectType +}) => { return ( - + ); diff --git a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx index 29ecacaa5..e4d7612ec 100644 --- a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx +++ b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { createNotification } from "@app/components/notifications"; import { useGetUserWorkspaces } from "@app/hooks/api"; -import { Workspace } from "@app/hooks/api/workspace/types"; +import { ProjectType, Workspace } from "@app/hooks/api/workspace/types"; type TWorkspaceContext = { workspaces: Workspace[]; @@ -35,7 +35,7 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => { const shouldTriggerNoProjectAccess = !value.isLoading && !value.currentWorkspace && - router.pathname.startsWith("/project") && + Object.values(ProjectType).some((el) => router.pathname.startsWith(`/${el}`)) && workspaceId; // handle redirects for project-specific routes diff --git a/frontend/src/helpers/parseEnvVar.ts b/frontend/src/helpers/parseEnvVar.ts index 27640b515..8bde05084 100644 --- a/frontend/src/helpers/parseEnvVar.ts +++ b/frontend/src/helpers/parseEnvVar.ts @@ -1,14 +1,31 @@ /** Extracts the key and value from a passed in env string based on the provided delimiters. */ export const getKeyValue = (pastedContent: string, delimiters: string[]) => { - const foundDelimiter = delimiters.find((delimiter) => pastedContent.includes(delimiter)); + if (!pastedContent) { + return { key: "", value: "" }; + } - if (!foundDelimiter) { + let firstDelimiterIndex = -1; + let foundDelimiter = ""; + + delimiters.forEach((delimiter) => { + const index = pastedContent.indexOf(delimiter); + if (index !== -1 && (firstDelimiterIndex === -1 || index < firstDelimiterIndex)) { + firstDelimiterIndex = index; + foundDelimiter = delimiter; + } + }); + + const hasValueAfterDelimiter = pastedContent.length > firstDelimiterIndex + foundDelimiter.length; + + if (firstDelimiterIndex === -1 || !hasValueAfterDelimiter) { return { key: pastedContent.trim(), value: "" }; } - const [key, value] = pastedContent.split(foundDelimiter); + const key = pastedContent.substring(0, firstDelimiterIndex); + const value = pastedContent.substring(firstDelimiterIndex + foundDelimiter.length); + return { key: key.trim(), - value: (value ?? "").trim() + value: value.trim() }; }; diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index b6338ce35..0734335e7 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -1,5 +1,6 @@ import { apiRequest } from "@app/config/request"; import { createWorkspace } from "@app/hooks/api/workspace/queries"; +import { ProjectType, Workspace } from "@app/hooks/api/workspace/types"; const secretsToBeAdded = [ { @@ -36,12 +37,13 @@ const secretsToBeAdded = [ * Create and initialize a new project in organization with id [organizationId] * Note: current user should be a member of the organization */ -const initProjectHelper = async ({ projectName }: { projectName: string }) => { +export const initProjectHelper = async ({ projectName }: { projectName: string }) => { // create new project const { data: { project } } = await createWorkspace({ - projectName + projectName, + type: ProjectType.SecretManager }); try { @@ -59,4 +61,27 @@ const initProjectHelper = async ({ projectName }: { projectName: string }) => { return project; }; -export { initProjectHelper }; +export const getProjectHomePage = (workspace: Workspace) => { + if (workspace.type === ProjectType.SecretManager) { + return `/${workspace.type}/${workspace.id}/secrets/overview`; + } + if (workspace.type === ProjectType.CertificateManager) { + return `/${workspace.type}/${workspace.id}/certificates`; + } + + if (workspace.type === ProjectType.KMS) { + return `/${workspace.type}/${workspace.id}/kms`; + } + + return `/${workspace.type}/${workspace.id}/ssh`; +}; + +export const getProjectTitle = (type: ProjectType) => { + const titleConvert = { + [ProjectType.SecretManager]: "Secret Management", + [ProjectType.KMS]: "Key Management", + [ProjectType.CertificateManager]: "Cert Management", + [ProjectType.SSH]: "SSH" + }; + return titleConvert[type]; +}; diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index 0c57ee82c..c11d7dc11 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -7,5 +7,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { [IdentityAuthMethod.GCP_AUTH]: "GCP Auth", [IdentityAuthMethod.AWS_AUTH]: "AWS Auth", [IdentityAuthMethod.AZURE_AUTH]: "Azure Auth", - [IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth" + [IdentityAuthMethod.OIDC_AUTH]: "OIDC 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 5e445521a..415492e00 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -5,5 +5,11 @@ export enum IdentityAuthMethod { GCP_AUTH = "gcp-auth", AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", - OIDC_AUTH = "oidc-auth" + OIDC_AUTH = "oidc-auth", + JWT_AUTH = "jwt-auth" +} + +export enum IdentityJwtConfigurationType { + JWKS = "jwks", + STATIC = "static" } diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index 5c7bcc3e7..261556752 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -4,6 +4,7 @@ export { useAddIdentityAwsAuth, useAddIdentityAzureAuth, useAddIdentityGcpAuth, + useAddIdentityJwtAuth, useAddIdentityKubernetesAuth, useAddIdentityOidcAuth, useAddIdentityTokenAuth, @@ -15,6 +16,7 @@ export { useDeleteIdentityAwsAuth, useDeleteIdentityAzureAuth, useDeleteIdentityGcpAuth, + useDeleteIdentityJwtAuth, useDeleteIdentityKubernetesAuth, useDeleteIdentityOidcAuth, useDeleteIdentityTokenAuth, @@ -25,20 +27,24 @@ export { useUpdateIdentityAwsAuth, useUpdateIdentityAzureAuth, useUpdateIdentityGcpAuth, + useUpdateIdentityJwtAuth, useUpdateIdentityKubernetesAuth, useUpdateIdentityOidcAuth, useUpdateIdentityTokenAuth, useUpdateIdentityTokenAuthToken, - useUpdateIdentityUniversalAuth} from "./mutations"; + useUpdateIdentityUniversalAuth +} from "./mutations"; export { useGetIdentityAwsAuth, useGetIdentityAzureAuth, useGetIdentityById, useGetIdentityGcpAuth, + useGetIdentityJwtAuth, useGetIdentityKubernetesAuth, useGetIdentityOidcAuth, useGetIdentityProjectMemberships, useGetIdentityTokenAuth, useGetIdentityTokensTokenAuth, useGetIdentityUniversalAuth, - useGetIdentityUniversalAuthClientSecrets} from "./queries"; + useGetIdentityUniversalAuthClientSecrets +} from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 21c4c560e..8daaae236 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -8,6 +8,7 @@ import { AddIdentityAwsAuthDTO, AddIdentityAzureAuthDTO, AddIdentityGcpAuthDTO, + AddIdentityJwtAuthDTO, AddIdentityKubernetesAuthDTO, AddIdentityOidcAuthDTO, AddIdentityTokenAuthDTO, @@ -22,6 +23,7 @@ import { DeleteIdentityAzureAuthDTO, DeleteIdentityDTO, DeleteIdentityGcpAuthDTO, + DeleteIdentityJwtAuthDTO, DeleteIdentityKubernetesAuthDTO, DeleteIdentityOidcAuthDTO, DeleteIdentityTokenAuthDTO, @@ -32,6 +34,7 @@ import { IdentityAwsAuth, IdentityAzureAuth, IdentityGcpAuth, + IdentityJwtAuth, IdentityKubernetesAuth, IdentityOidcAuth, IdentityTokenAuth, @@ -42,6 +45,7 @@ import { UpdateIdentityAzureAuthDTO, UpdateIdentityDTO, UpdateIdentityGcpAuthDTO, + UpdateIdentityJwtAuthDTO, UpdateIdentityKubernetesAuthDTO, UpdateIdentityOidcAuthDTO, UpdateIdentityTokenAuthDTO, @@ -518,6 +522,118 @@ export const useDeleteIdentityOidcAuth = () => { } }); }; +export const useUpdateIdentityJwtAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject + }) => { + const { + data: { identityJwtAuth } + } = await apiRequest.patch<{ identityJwtAuth: IdentityJwtAuth }>( + `/api/v1/auth/jwt-auth/identities/${identityId}`, + { + configurationType, + jwksUrl, + jwksCaCert, + publicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityJwtAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityJwtAuth(identityId)); + } + }); +}; + +export const useAddIdentityJwtAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityJwtAuth } + } = await apiRequest.post<{ identityJwtAuth: IdentityJwtAuth }>( + `/api/v1/auth/jwt-auth/identities/${identityId}`, + { + configurationType, + jwksUrl, + jwksCaCert, + publicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityJwtAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityJwtAuth(identityId)); + } + }); +}; + +export const useDeleteIdentityJwtAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityJwtAuth } + } = await apiRequest.delete(`/api/v1/auth/jwt-auth/identities/${identityId}`); + return identityJwtAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId)); + queryClient.invalidateQueries(identitiesKeys.getIdentityJwtAuth(identityId)); + } + }); +}; export const useAddIdentityAzureAuth = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index c5c442407..49136614e 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -8,6 +8,7 @@ import { IdentityAwsAuth, IdentityAzureAuth, IdentityGcpAuth, + IdentityJwtAuth, IdentityKubernetesAuth, IdentityMembership, IdentityMembershipOrg, @@ -29,6 +30,7 @@ export const identitiesKeys = { getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const, 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, getIdentityTokensTokenAuth: (identityId: string) => [{ identityId }, "identity-tokens-token-auth"] as const, getIdentityProjectMemberships: (identityId: string) => @@ -276,3 +278,30 @@ export const useGetIdentityOidcAuth = ( enabled: Boolean(identityId) && (options?.enabled ?? true) }); }; + +export const useGetIdentityJwtAuth = ( + identityId: string, + options?: UseQueryOptions< + IdentityJwtAuth, + unknown, + IdentityJwtAuth, + ReturnType + > +) => { + return useQuery({ + queryKey: identitiesKeys.getIdentityJwtAuth(identityId), + queryFn: async () => { + const { + data: { identityJwtAuth } + } = await apiRequest.get<{ identityJwtAuth: IdentityJwtAuth }>( + `/api/v1/auth/jwt-auth/identities/${identityId}` + ); + + return identityJwtAuth; + }, + staleTime: 0, + cacheTime: 0, + ...options, + enabled: Boolean(identityId) && (options?.enabled ?? true) + }); +}; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 559a01974..8d344e7f7 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -1,6 +1,6 @@ import { TOrgRole } from "../roles/types"; import { ProjectUserMembershipTemporaryMode, Workspace } from "../workspace/types"; -import { IdentityAuthMethod } from "./enums"; +import { IdentityAuthMethod, IdentityJwtConfigurationType } from "./enums"; export type IdentityTrustedIp = { id: string; @@ -47,7 +47,7 @@ export type IdentityMembershipOrg = { export type IdentityMembership = { id: string; identity: Identity; - project: Pick; + project: Pick; roles: Array< { id: string; @@ -446,6 +446,65 @@ export type DeleteIdentityTokenAuthDTO = { identityId: string; }; +export type IdentityJwtAuth = { + identityId: string; + configurationType: IdentityJwtConfigurationType; + jwksUrl: string; + jwksCaCert: string; + publicKeys: string[]; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + +export type AddIdentityJwtAuthDTO = { + organizationId: string; + identityId: string; + configurationType: string; + jwksUrl?: string; + jwksCaCert: string; + publicKeys?: string[]; + boundIssuer: string; + boundAudiences: string; + boundClaims: Record; + boundSubject: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityJwtAuthDTO = { + organizationId: string; + identityId: string; + configurationType?: string; + jwksUrl?: string; + jwksCaCert?: string; + publicKeys?: string[]; + boundIssuer?: string; + boundAudiences?: string; + boundClaims?: Record; + boundSubject?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + +export type DeleteIdentityJwtAuthDTO = { + organizationId: string; + identityId: string; +}; + export type CreateTokenIdentityTokenAuthDTO = { identityId: string; name: string; diff --git a/frontend/src/hooks/api/migration/mutations.tsx b/frontend/src/hooks/api/migration/mutations.tsx index 41d17b0bd..feee7778d 100644 --- a/frontend/src/hooks/api/migration/mutations.tsx +++ b/frontend/src/hooks/api/migration/mutations.tsx @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace"; +import { ProjectType } from "../workspace/types"; export const useImportEnvKey = () => { const queryClient = useQueryClient(); @@ -31,7 +32,7 @@ export const useImportEnvKey = () => { } }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 594287c9c..319fb723d 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -1,6 +1,6 @@ import { MfaMethod } from "../auth/types"; import { UserWsKeyPair } from "../keys/types"; -import { ProjectUserMembershipTemporaryMode } from "../workspace/types"; +import { ProjectType, ProjectUserMembershipTemporaryMode } from "../workspace/types"; export enum AuthMethod { EMAIL = "email", @@ -95,6 +95,7 @@ export type TWorkspaceUser = { project: { id: string; name: string; + type: ProjectType; }; inviteEmail: string; organization: string; diff --git a/frontend/src/hooks/api/workspace/mutations.tsx b/frontend/src/hooks/api/workspace/mutations.tsx index ae8829591..c4b8211bb 100644 --- a/frontend/src/hooks/api/workspace/mutations.tsx +++ b/frontend/src/hooks/api/workspace/mutations.tsx @@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request"; import { userKeys } from "../users/query-keys"; import { workspaceKeys } from "./query-keys"; -import { TUpdateWorkspaceGroupRoleDTO } from "./types"; +import { ProjectType, TUpdateWorkspaceGroupRoleDTO } from "./types"; export const useAddGroupToWorkspace = () => { const queryClient = useQueryClient(); @@ -83,7 +83,7 @@ export const useLeaveProject = () => { return apiRequest.delete(`/api/v1/workspace/${workspaceId}/leave`); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace()); } }); }; @@ -95,7 +95,7 @@ export const useMigrateProjectToV3 = () => { return apiRequest.post(`/api/v1/workspace/${workspaceId}/migrate-v3`); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 3385344cd..45524bdb3 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -28,6 +28,7 @@ import { DeleteWorkspaceDTO, NameWorkspaceSecretsDTO, ProjectIdentityOrderBy, + ProjectType, TGetUpgradeProjectStatusDTO, TListProjectIdentitiesDTO, ToggleAutoCapitalizationDTO, @@ -84,7 +85,7 @@ export const useUpgradeProject = () => { }); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; @@ -104,10 +105,11 @@ export const useGetUpgradeProjectStatus = ({ }); }; -const fetchUserWorkspaces = async (includeRoles?: boolean) => { +const fetchUserWorkspaces = async (includeRoles?: boolean, type?: ProjectType | "all") => { const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", { params: { - includeRoles + includeRoles, + type } }); return data.workspaces; @@ -141,8 +143,16 @@ export const useGetWorkspaceById = ( }); }; -export const useGetUserWorkspaces = (includeRoles?: boolean) => - useQuery(workspaceKeys.getAllUserWorkspace, () => fetchUserWorkspaces(includeRoles)); +export const useGetUserWorkspaces = ({ + includeRoles, + type = "all" +}: { + includeRoles?: boolean; + type?: ProjectType | "all"; +} = {}) => + useQuery(workspaceKeys.getAllUserWorkspace(type || ""), () => + fetchUserWorkspaces(includeRoles, type) + ); const fetchUserWorkspaceMemberships = async (orgId: string) => { const { data } = await apiRequest.get>( @@ -208,33 +218,26 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) => refetchInterval: 4000 }); -export const createWorkspace = ({ - projectName, - projectDescription, - kmsKeyId, - template -}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => { - return apiRequest.post("/api/v2/workspace", { - projectName, - projectDescription, - kmsKeyId, - template - }); +export const createWorkspace = ( + dto: CreateWorkspaceDTO +): Promise<{ data: { project: Workspace } }> => { + return apiRequest.post("/api/v2/workspace", dto); }; export const useCreateWorkspace = () => { const queryClient = useQueryClient(); return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({ - mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) => + mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) => createWorkspace({ projectName, projectDescription, kmsKeyId, - template + template, + type }), - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.data.project.type)); } }); }; @@ -242,15 +245,19 @@ export const useCreateWorkspace = () => { export const useUpdateProject = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, UpdateProjectDTO>({ - mutationFn: ({ projectID, newProjectName, newProjectDescription }) => { - return apiRequest.patch(`/api/v1/workspace/${projectID}`, { - name: newProjectName, - description: newProjectDescription - }); + return useMutation({ + mutationFn: async ({ projectID, newProjectName, newProjectDescription }) => { + const { data } = await apiRequest.patch<{ workspace: Workspace }>( + `/api/v1/workspace/${projectID}`, + { + name: newProjectName, + description: newProjectDescription + } + ); + return data.workspace; }, - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); } }); }; @@ -258,13 +265,18 @@ export const useUpdateProject = () => { export const useToggleAutoCapitalization = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({ - mutationFn: ({ workspaceID, state }) => - apiRequest.post(`/api/v1/workspace/${workspaceID}/auto-capitalization`, { - autoCapitalization: state - }), - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + return useMutation({ + mutationFn: async ({ workspaceID, state }) => { + const { data } = await apiRequest.post<{ workspace: Workspace }>( + `/api/v1/workspace/${workspaceID}/auto-capitalization`, + { + autoCapitalization: state + } + ); + return data.workspace; + }, + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); } }); }; @@ -272,14 +284,15 @@ export const useToggleAutoCapitalization = () => { export const useUpdateWorkspaceVersionLimit = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, UpdatePitVersionLimitDTO>({ - mutationFn: ({ projectSlug, pitVersionLimit }) => { - return apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, { + return useMutation({ + mutationFn: async ({ projectSlug, pitVersionLimit }) => { + const { data } = await apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, { pitVersionLimit }); + return data.workspace; }, - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); } }); }; @@ -287,14 +300,18 @@ export const useUpdateWorkspaceVersionLimit = () => { export const useUpdateWorkspaceAuditLogsRetention = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, UpdateAuditLogsRetentionDTO>({ - mutationFn: ({ projectSlug, auditLogsRetentionDays }) => { - return apiRequest.put(`/api/v1/workspace/${projectSlug}/audit-logs-retention`, { - auditLogsRetentionDays - }); + return useMutation({ + mutationFn: async ({ projectSlug, auditLogsRetentionDays }) => { + const { data } = await apiRequest.put( + `/api/v1/workspace/${projectSlug}/audit-logs-retention`, + { + auditLogsRetentionDays + } + ); + return data.workspace; }, - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); } }); }; @@ -302,12 +319,13 @@ export const useUpdateWorkspaceAuditLogsRetention = () => { export const useDeleteWorkspace = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, DeleteWorkspaceDTO>({ - mutationFn: ({ workspaceID }) => { - return apiRequest.delete(`/api/v1/workspace/${workspaceID}`); + return useMutation({ + mutationFn: async ({ workspaceID }) => { + const { data } = await apiRequest.delete(`/api/v1/workspace/${workspaceID}`); + return data.workspace; }, - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); queryClient.invalidateQueries(["org-admin-projects"]); } }); @@ -324,7 +342,7 @@ export const useCreateWsEnvironment = () => { }); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; @@ -341,7 +359,7 @@ export const useUpdateWsEnvironment = () => { }); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; @@ -354,7 +372,7 @@ export const useDeleteWsEnvironment = () => { return apiRequest.delete(`/api/v1/workspace/${workspaceId}/environments/${id}`); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index bec32ceeb..1005fe8bc 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -11,7 +11,8 @@ export const workspaceKeys = { getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"], getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"], getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"], - getAllUserWorkspace: ["workspaces"] as const, + getAllUserWorkspace: (type?: string) => + type ? ["workspaces", { type }] : (["workspace"] as const), getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }, "workspace-audit-logs"] as const, getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const, diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 91cf3a9d8..0510bdbe7 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -8,6 +8,13 @@ export enum ProjectVersion { V3 = 3 } +export enum ProjectType { + SecretManager = "secret-manager", + CertificateManager = "cert-manager", + KMS = "kms", + SSH = "ssh" +} + export enum ProjectUserMembershipTemporaryMode { Relative = "relative" } @@ -16,6 +23,7 @@ export type Workspace = { __v: number; id: string; name: string; + type: ProjectType; description?: string; orgId: string; version: ProjectVersion; @@ -59,6 +67,7 @@ export type CreateWorkspaceDTO = { projectDescription?: string; kmsKeyId?: string; template?: string; + type: ProjectType; }; export type UpdateProjectDTO = { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index b7ff48788..918ec415a 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -5,7 +5,7 @@ /* eslint-disable no-var */ /* eslint-disable func-names */ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -38,21 +38,17 @@ import { } from "@app/components/v2"; import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; -import { - useGetAccessRequestsCount, - useGetOrgTrialUrl, - useGetSecretApprovalRequestCount, - useLogoutUser, - useSelectOrganization -} from "@app/hooks/api"; +import { useGetOrgTrialUrl, useLogoutUser, useSelectOrganization } from "@app/hooks/api"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { AuthMethod } from "@app/hooks/api/users/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { InsecureConnectionBanner } from "@app/layouts/AppLayout/components/InsecureConnectionBanner"; import { ProjectSelect } from "@app/layouts/AppLayout/components/ProjectSelect"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; import { Mfa } from "@app/views/Login/Mfa"; import { CreateOrgModal } from "@app/views/Org/components"; +import { ProjectSidebarItem } from "./components/ProjectSidebarItems"; import { WishForm } from "./components/WishForm/WishForm"; interface LayoutProps { @@ -87,7 +83,7 @@ export const AppLayout = ({ children }: LayoutProps) => { const { mutateAsync } = useGetOrgTrialUrl(); - const { workspaces, currentWorkspace } = useWorkspace(); + const { currentWorkspace } = useWorkspace(); const { orgs, currentOrg } = useOrganization(); const [shouldShowMfa, toggleShowMfa] = useToggle(false); @@ -96,15 +92,6 @@ export const AppLayout = ({ children }: LayoutProps) => { const { user } = useUser(); const { subscription } = useSubscription(); - const workspaceId = currentWorkspace?.id || ""; - const projectSlug = currentWorkspace?.slug || ""; - - const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId }); - const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug }); - - const pendingRequestsCount = useMemo(() => { - return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); - }, [secretApprovalReqCount, accessApprovalRequestCount]); const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION; @@ -152,41 +139,6 @@ export const AppLayout = ({ children }: LayoutProps) => { if (tempLocalStorage("orgData.id") === "" && orgs?.[0]?.id) { localStorage.setItem("orgData.id", orgs?.[0]?.id); } - - if ( - currentOrg && - ((workspaces?.length === 0 && router.asPath.includes("project")) || - router.asPath.includes("/project/undefined") || - (!orgs?.map((org) => org.id)?.includes(router.query.id as string) && - !router.asPath.includes("project") && - !router.asPath.includes("personal") && - !router.asPath.includes("secret-scanning") && - !router.asPath.includes("integration"))) - ) { - router.push(`/org/${currentOrg?.id}/overview`); - } - // else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) { - - // const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0); - - // let intendedWorkspaceId; - // if (pathSegments.length >= 2 && pathSegments[0] === "dashboard") { - // [, intendedWorkspaceId] = pathSegments; - // } else if (pathSegments.length >= 3 && pathSegments[0] === "settings") { - // [, , intendedWorkspaceId] = pathSegments; - // } else { - // const lastPathSegments = router.asPath.split("/").pop(); - // if (lastPathSegments !== undefined) { - // [intendedWorkspaceId] = lastPathSegments.split("?"); - // } - // } - - // if (!intendedWorkspaceId) return; - - // if (!["callback", "create", "authorize"].includes(intendedWorkspaceId)) { - // localStorage.setItem("projectData.id", intendedWorkspaceId); - // } - // } }; putUserInOrg(); }, [router.query.id]); @@ -214,9 +166,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
{!router.asPath.includes("personal") && (
- {(router.asPath.includes("project") || - router.asPath.includes("integrations")) && ( - + {(currentWorkspace || router.asPath.includes("integrations")) && ( +
@@ -379,7 +330,7 @@ export const AppLayout = ({ children }: LayoutProps) => { (!router.asPath.includes("personal") && currentWorkspace ? ( ) : ( - +
Back to organization @@ -387,132 +338,61 @@ export const AppLayout = ({ children }: LayoutProps) => { ))}
- {(router.asPath.includes("project") || router.asPath.includes("integrations")) && - currentWorkspace ? ( - - + + {router.pathname.startsWith("/org") && ( + + - {t("nav.menu.secrets")} + Secret Management - + - Internal PKI + Cert Management - + Key Management - + SSH - - - - Access Control - - - - - - - {t("nav.menu.integrations")} - - - - - - - Secret Rotation - - - - - - - Approvals - {Boolean( - secretApprovalReqCount?.open || - accessApprovalRequestCount?.pendingCount - ) && ( - - {pendingRequestsCount} - - )} - - - - - - - {t("nav.menu.project-settings")} - - - - - ) : ( - - - - - Overview - - - { size="xs" onClick={() => // we need to go to /org/${orgId}/overview, but we need to do a full page reload to ensure that the error the user is facing is properly reset. - window.location.assign(`/org/${orgId}/overview`) + window.location.assign(`/org/${orgId}/${ProjectType.SecretManager}/overview`) } > diff --git a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx deleted file mode 100644 index 979758ba5..000000000 --- a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx +++ /dev/null @@ -1,343 +0,0 @@ -/* eslint-disable jsx-a11y/anchor-is-valid */ -/* eslint-disable react/jsx-key */ -import { Fragment, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons"; -import { - faAngleDown, - faBook, - faCoins, - faEnvelope, - faGear, - faPlus, - faRightFromBracket -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Menu, Transition } from "@headlessui/react"; -import { TFunction } from "i18next"; - -import guidGenerator from "@app/components/utilities/randomId"; -import { useOrganization, useSubscription, useUser } from "@app/context"; -import { useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api"; - -const supportOptions = (t: TFunction) => [ - [ - , - t("nav.support.slack"), - "https://infisical.com/slack" - ], - [ - , - t("nav.support.docs"), - "https://infisical.com/docs/documentation/getting-started/introduction" - ], - [ - , - t("nav.support.issue"), - "https://github.com/Infisical/infisical-cli/issues" - ], - [ - , - t("nav.support.email"), - "mailto:support@infisical.com" - ] -]; - -export interface ICurrentOrg { - name: string; -} - -export interface IUser { - firstName: string; - lastName: string; - email: string; -} - -/** - * This is the navigation bar in the main app. - * It has two main components: support options and user menu (inlcudes billing, logout, org/user settings) - * @returns NavBar - */ -export const Navbar = () => { - const router = useRouter(); - const { subscription } = useSubscription(); - - const { currentOrg, orgs } = useOrganization(); - const { mutateAsync } = useGetOrgTrialUrl(); - const { user } = useUser(); - - const logout = useLogoutUser(); - - const { t } = useTranslation(); - - // remove this memo - const supportOptionsList = useMemo(() => supportOptions(t), [t]); - - const closeApp = async () => { - try { - console.log("Logging out..."); - await logout.mutateAsync(); - localStorage.removeItem("protectedKey"); - localStorage.removeItem("protectedKeyIV"); - localStorage.removeItem("protectedKeyTag"); - localStorage.removeItem("publicKey"); - localStorage.removeItem("encryptedPrivateKey"); - localStorage.removeItem("iv"); - localStorage.removeItem("tag"); - localStorage.removeItem("PRIVATE_KEY"); - localStorage.removeItem("orgData.id"); - localStorage.removeItem("projectData.id"); - router.push("/login"); - } catch (error) { - console.error(error); - } - }; - - return ( -
-
- -
- - - Docs - - -
- - - -
- - - {supportOptionsList.map(([icon, text, url]) => ( - -
- {icon} -
{text}
-
-
- ))} -
-
-
- -
- - {user?.firstName} {user?.lastName} - - -
- - -
-
- {t("nav.user.signed-in-as")} -
-
null} - role="button" - tabIndex={0} - onClick={() => router.push("/personal-settings")} - className="mx-1 my-1 flex cursor-pointer flex-row items-center rounded-md px-1 hover:bg-white/5" - > -
- {user?.firstName?.charAt(0)} -
-
-
-

- {" "} - {user?.firstName} {user?.lastName} -

-

{user?.email}

-
- -
-
-
-
-
- {t("nav.user.current-organization")} -
-
null} - role="button" - tabIndex={0} - onClick={() => router.push(`/settings/org/${router.query.id}`)} - className="mt-2 flex cursor-pointer flex-row items-center rounded-md px-2 py-1 hover:bg-white/5" - > -
- {currentOrg?.name?.charAt(0)} -
-
-

{currentOrg?.name}

- -
-
- {subscription && subscription.slug !== null && ( - - )} - -
- {orgs && orgs?.length > 1 && ( -
-
- {t("nav.user.other-organizations")} -
-
- {orgs - ?.filter((org: { id: string }) => org.id !== currentOrg?.id) - .map((org: { id: string; name: string }) => ( -
null} - role="button" - tabIndex={0} - key={guidGenerator()} - onClick={() => { - localStorage.setItem("orgData.id", org.id); - router.reload(); - }} - className="flex w-full cursor-pointer flex-row items-center justify-start rounded-md p-1.5 hover:bg-white/5" - > -
- {org.name.charAt(0)} -
-
-

{org.name}

-
-
- ))} -
-
- )} -
- - {({ active }) => ( - - )} - -
-
-
-
-
-
- {subscription && subscription.slug === "starter" && !subscription.has_used_trial && ( -
- -
- )} -
- ); -}; diff --git a/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx b/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx index 777d65b71..b351d8ae5 100644 --- a/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx +++ b/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx @@ -16,10 +16,11 @@ import { useSubscription, useWorkspace } from "@app/context"; +import { getProjectHomePage, getProjectTitle } from "@app/helpers/project"; import { usePopUp } from "@app/hooks"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; -import { Workspace } from "@app/hooks/api/workspace/types"; +import { ProjectType, Workspace } from "@app/hooks/api/workspace/types"; type TWorkspaceWithFaveProp = Workspace & { isFavorite: boolean }; @@ -138,6 +139,7 @@ export const ProjectSelect = () => { const { options, value } = useMemo(() => { const projectOptions = workspaces + .filter((el) => el.type === currentWorkspace?.type) .map((w): Workspace & { isFavorite: boolean } => ({ ...w, isFavorite: Boolean(projectFavorites?.includes(w.id)) @@ -164,7 +166,9 @@ export const ProjectSelect = () => { return (
-

Project

+

+ {currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"} +

{ // todo(akhi): this is not using react query because react query in overview is throwing error when envs are not exact same count // to reproduce change this back to router.push and switch between two projects with different env count // look into this on dashboard revamp - window.location.assign(`/project/${project.id}/secrets/overview`); + window.location.assign(getProjectHomePage(project)); }} options={options} components={{ @@ -206,6 +210,7 @@ export const ProjectSelect = () => { handlePopUpToggle("addNewWs", isOpen)} + projectType={currentWorkspace?.type || ProjectType.SecretManager} />
); diff --git a/frontend/src/layouts/AppLayout/components/ProjectSidebarItems/ProjectSidebarItems.tsx b/frontend/src/layouts/AppLayout/components/ProjectSidebarItems/ProjectSidebarItems.tsx new file mode 100644 index 000000000..9cd3d568f --- /dev/null +++ b/frontend/src/layouts/AppLayout/components/ProjectSidebarItems/ProjectSidebarItems.tsx @@ -0,0 +1,171 @@ +import { useTranslation } from "react-i18next"; +import Link from "next/link"; +import { useRouter } from "next/router"; + +import { Menu, MenuItem } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api"; +import { ProjectType } from "@app/hooks/api/workspace/types"; + +export const ProjectSidebarItem = () => { + const { currentWorkspace } = useWorkspace(); + const router = useRouter(); + const { t } = useTranslation(); + + const workspaceId = currentWorkspace?.id || ""; + const projectSlug = currentWorkspace?.slug || ""; + + const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId }); + const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug }); + + const pendingRequestsCount = + (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); + + if ( + !currentWorkspace || + router.asPath.startsWith("personal") || + router.asPath.startsWith("integrations") + ) { + return
; + } + + const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager; + const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager; + const isCmek = currentWorkspace?.type === ProjectType.KMS; + const isSsh = currentWorkspace?.type === ProjectType.SSH; + + return ( + + {isSecretManager && ( + + + + {t("nav.menu.secrets")} + + + + )} + {isCertManager && ( + + + + Overview + + + + )} + {isCmek && ( + + + + Overview + + + + )} + {isSsh && ( + + + + Overview + + + + )} + + + + Access Control + + + + {isSecretManager && ( + + + + {t("nav.menu.integrations")} + + + + )} + {isSecretManager && ( + + + + Secret Rotation + + + + )} + {isSecretManager && ( + + + + Approvals + {Boolean( + secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount + ) && ( + + {pendingRequestsCount} + + )} + + + + )} + + + + {t("nav.menu.project-settings")} + + + + + ); +}; diff --git a/frontend/src/layouts/AppLayout/components/ProjectSidebarItems/index.tsx b/frontend/src/layouts/AppLayout/components/ProjectSidebarItems/index.tsx new file mode 100644 index 000000000..70b5840b3 --- /dev/null +++ b/frontend/src/layouts/AppLayout/components/ProjectSidebarItems/index.tsx @@ -0,0 +1 @@ +export { ProjectSidebarItem } from "./ProjectSidebarItems"; diff --git a/frontend/src/layouts/AppLayout/components/WishForm/WishForm.tsx b/frontend/src/layouts/AppLayout/components/WishForm/WishForm.tsx index bf87d0672..91907ee8c 100644 --- a/frontend/src/layouts/AppLayout/components/WishForm/WishForm.tsx +++ b/frontend/src/layouts/AppLayout/components/WishForm/WishForm.tsx @@ -65,7 +65,7 @@ export const WishForm = () => {
- Make a wish + Request a feature
{ + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: t("settings.project.title") })} + + + + + ); +}; + +export default ProjectAllowlist; + +ProjectAllowlist.requireAuth = true; diff --git a/frontend/src/pages/kms/[id]/identities/[identityId]/index.tsx b/frontend/src/pages/kms/[id]/identities/[identityId]/index.tsx new file mode 100644 index 000000000..ae8b2716a --- /dev/null +++ b/frontend/src/pages/kms/[id]/identities/[identityId]/index.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { IdentityDetailsPage } from "@app/views/Project/IdentityDetailsPage"; + +export default function ProjectIdentityDetailsPage() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +ProjectIdentityDetailsPage.requireAuth = true; diff --git a/frontend/src/pages/project/[id]/kms/index.tsx b/frontend/src/pages/kms/[id]/kms/index.tsx similarity index 100% rename from frontend/src/pages/project/[id]/kms/index.tsx rename to frontend/src/pages/kms/[id]/kms/index.tsx diff --git a/frontend/src/pages/kms/[id]/members/[membershipId]/index.tsx b/frontend/src/pages/kms/[id]/members/[membershipId]/index.tsx new file mode 100644 index 000000000..033aa2b15 --- /dev/null +++ b/frontend/src/pages/kms/[id]/members/[membershipId]/index.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage"; + +export default function Page() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +Page.requireAuth = true; diff --git a/frontend/src/pages/kms/[id]/members/index.tsx b/frontend/src/pages/kms/[id]/members/index.tsx new file mode 100644 index 000000000..4bcbb833e --- /dev/null +++ b/frontend/src/pages/kms/[id]/members/index.tsx @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { MembersPage } from "@app/views/Project/MembersPage"; + +export default function WorkspaceMemberSettings() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +WorkspaceMemberSettings.requireAuth = true; diff --git a/frontend/src/pages/kms/[id]/roles/[roleSlug]/index.tsx b/frontend/src/pages/kms/[id]/roles/[roleSlug]/index.tsx new file mode 100644 index 000000000..17c854cee --- /dev/null +++ b/frontend/src/pages/kms/[id]/roles/[roleSlug]/index.tsx @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { RolePage } from "@app/views/Project/RolePage"; + +export default function Role() { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "Project Settings" })} + + + + + ); +} + +Role.requireAuth = true; diff --git a/frontend/src/pages/kms/[id]/settings/index.tsx b/frontend/src/pages/kms/[id]/settings/index.tsx new file mode 100644 index 000000000..331ba8cc5 --- /dev/null +++ b/frontend/src/pages/kms/[id]/settings/index.tsx @@ -0,0 +1,22 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { ProjectSettingsPage } from "@app/views/Settings/ProjectSettingsPage"; + +const ProjectSettings = () => { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.project.title") })} + + + + + ); +}; + +export default ProjectSettings; + +ProjectSettings.requireAuth = true; diff --git a/frontend/src/pages/org/[id]/cert-manager/overview.tsx b/frontend/src/pages/org/[id]/cert-manager/overview.tsx new file mode 100644 index 000000000..07f00833e --- /dev/null +++ b/frontend/src/pages/org/[id]/cert-manager/overview.tsx @@ -0,0 +1,9 @@ +import { ProjectType } from "@app/hooks/api/workspace/types"; + +import { ProductOverview } from "../secret-manager/overview"; + +const CertManagerOverviewPage = () => ; + +Object.assign(CertManagerOverviewPage, { requireAuth: true }); + +export default CertManagerOverviewPage; diff --git a/frontend/src/pages/org/[id]/kms/overview.tsx b/frontend/src/pages/org/[id]/kms/overview.tsx new file mode 100644 index 000000000..c412207dc --- /dev/null +++ b/frontend/src/pages/org/[id]/kms/overview.tsx @@ -0,0 +1,9 @@ +import { ProjectType } from "@app/hooks/api/workspace/types"; + +import { ProductOverview } from "../secret-manager/overview"; + +const CmekManagerOverviewPage = () => ; + +Object.assign(CmekManagerOverviewPage, { requireAuth: true }); + +export default CmekManagerOverviewPage; diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 9e39fd389..68e19c8b1 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -1,1042 +1,20 @@ -// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex - -import { ReactNode, useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import Head from "next/head"; -import Link from "next/link"; +import { useEffect } from "react"; import { useRouter } from "next/router"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; -import { faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons"; -import { - faArrowDownAZ, - faArrowRight, - faArrowUpRightFromSquare, - faArrowUpZA, - faBorderAll, - faCheck, - faCheckCircle, - faClipboard, - faExclamationCircle, - faHandPeace, - faList, - faMagnifyingGlass, - faNetworkWired, - faPlug, - faPlus, - faSearch, - faStar as faSolidStar, - faUserPlus -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import * as Tabs from "@radix-ui/react-tabs"; -import { createNotification } from "@app/components/notifications"; -import { OrgPermissionCan } from "@app/components/permissions"; -import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; -import { - Button, - IconButton, - Input, - Pagination, - Skeleton, - Tooltip, - UpgradePlanModal -} from "@app/components/v2"; -import { NewProjectModal } from "@app/components/v2/projects"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useOrganization, - useSubscription, - useUser, - useWorkspace -} from "@app/context"; -import { usePagination, useResetPageHelper } from "@app/hooks"; -import { useRegisterUserAction } from "@app/hooks/api"; -import { OrderByDirection } from "@app/hooks/api/generic/types"; -// import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; -import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; -import { Workspace } from "@app/hooks/api/types"; -import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; -import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; -import { usePopUp } from "@app/hooks/usePopUp"; - -const features = [ - { - id: 0, - name: "Kubernetes Operator", - link: "https://infisical.com/docs/documentation/getting-started/kubernetes", - description: - "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes." - }, - { - id: 1, - name: "Infisical Agent", - link: "https://infisical.com/docs/infisical-agent/overview", - description: "Inject secrets into your apps without modifying any application logic." - } -]; - -type ItemProps = { - text: string; - subText: string; - complete: boolean; - icon: IconProp; - time: string; - userAction?: string; - link?: string; -}; - -enum ProjectsViewMode { - GRID = "grid", - LIST = "list" -} - -enum ProjectOrderBy { - Name = "name" -} - -function copyToClipboard(id: string, setState: (value: boolean) => void) { - // Get the text field - const copyText = document.getElementById(id) as HTMLInputElement; - - // Select the text field - copyText.select(); - copyText.setSelectionRange(0, 99999); // For mobile devices - - // Copy the text inside the text field - navigator.clipboard.writeText(copyText.value); - - setState(true); - setTimeout(() => setState(false), 2000); - // Alert the copied text - // alert("Copied the text: " + copyText.value); -} - -const CodeItem = ({ - isCopied, - setIsCopied, - textExplanation, - code, - id -}: { - isCopied: boolean; - setIsCopied: (value: boolean) => void; - textExplanation: string; - code: string; - id: string; -}) => { - return ( - <> -

{textExplanation}

-
- - -
- - ); -}; - -const TabsObject = () => { - const [downloadCodeCopied, setDownloadCodeCopied] = useState(false); - const [downloadCode2Copied, setDownloadCode2Copied] = useState(false); - const [loginCodeCopied, setLoginCodeCopied] = useState(false); - const [initCodeCopied, setInitCodeCopied] = useState(false); - const [runCodeCopied, setRunCodeCopied] = useState(false); - - return ( - - - - MacOS - - - Windows - - {/* - Arch Linux - */} - - Other Platforms - - - - - - - -

- You can find example of start commands for different frameworks{" "} - - here - - .{" "} -

-
- - -
- - -
- - - -

- You can find example of start commands for different frameworks{" "} - - here - - .{" "} -

-
-
- ); -}; - -const LearningItem = ({ - text, - subText, - complete, - icon, - time, - userAction, - link -}: ItemProps): JSX.Element => { - const registerUserAction = useRegisterUserAction(); - if (link) { - return ( - -
-
null} - role="button" - tabIndex={0} - onClick={async () => { - if (userAction && userAction !== "first_time_secrets_pushed") { - await registerUserAction.mutateAsync(userAction); - } - }} - className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${ - complete - ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" - : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" - } text-mineshaft-100 duration-200`} - > -
- - {complete && ( -
- -
- )} -
-
{text}
-
{subText}
-
-
-
- {complete ? "Complete!" : `About ${time}`} -
- {/* {complete &&
} */} -
-
-
- ); - } - return ( -
null} - role="button" - tabIndex={0} - onClick={async () => { - if (userAction) { - await registerUserAction.mutateAsync(userAction); - } - }} - className="relative my-1.5 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-dashed border-bunker-400 bg-bunker-700 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-bunker-500" - > -
- - {complete && ( -
- -
- )} -
-
{text}
-
{subText}
-
-
-
- {complete ? "Complete!" : `About ${time}`} -
- {complete &&
} -
- ); -}; - -const LearningItemSquare = ({ - text, - subText, - complete, - icon, - time, - userAction, - link -}: ItemProps): JSX.Element => { - const registerUserAction = useRegisterUserAction(); - return ( - -
-
null} - role="button" - tabIndex={0} - onClick={async () => { - if (userAction && userAction !== "first_time_secrets_pushed") { - await registerUserAction.mutateAsync(userAction); - } - }} - className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${ - complete - ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" - : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" - } text-mineshaft-100 duration-200`} - > -
-
- - {complete && ( -
- -
- )} -
- {complete ? "Complete!" : `About ${time}`} -
-
-
-
{text}
-
{subText}
-
-
-
-
-
- ); -}; +import { useOrganization } from "@app/context"; +import { ProjectType } from "@app/hooks/api/workspace/types"; // #TODO: Update all the workspaceIds const OrganizationPage = () => { - const { t } = useTranslation(); - const router = useRouter(); - - const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace(); const { currentOrg } = useOrganization(); - const routerOrgId = String(router.query.id); - const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || []; - const { data: projectFavorites, isLoading: isProjectFavoritesLoading } = - useGetUserProjectFavorites(currentOrg?.id!); - const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); - - const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading; - - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ - "addNewWs", - "upgradePlan" - ] as const); - - const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); - const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); - const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); - const [usersInOrg, setUsersInOrg] = useState(false); - const [searchFilter, setSearchFilter] = useState(""); - const { user } = useUser(); - const { data: serverDetails } = useFetchServerStatus(); - const [projectsViewMode, setProjectsViewMode] = useState( - (localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID - ); - - const { subscription } = useSubscription(); - - const isAddingProjectsAllowed = subscription?.workspaceLimit - ? subscription.workspacesUsed < subscription.workspaceLimit - : true; - useEffect(() => { - onboardingCheck({ - orgId: routerOrgId, - setHasUserClickedIntro, - setHasUserClickedSlack, - setHasUserPushedSecrets, - setUsersInOrg - }); - }, []); - - const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0; - - const { - setPage, - perPage, - setPerPage, - page, - offset, - limit, - toggleOrderDirection, - orderDirection - } = usePagination(ProjectOrderBy.Name, { initPerPage: 24 }); - - const filteredWorkspaces = useMemo( - () => - orgWorkspaces - .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())) - .sort((a, b) => - orderDirection === OrderByDirection.ASC - ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) - : b.name.toLowerCase().localeCompare(a.name.toLowerCase()) - ), - [searchFilter, page, perPage, orderDirection, offset, limit] - ); - - useResetPageHelper({ - setPage, - offset, - totalCount: filteredWorkspaces.length - }); - - const { workspacesWithFaveProp } = useMemo(() => { - const workspacesWithFav = filteredWorkspaces - .map((w): Workspace & { isFavorite: boolean } => ({ - ...w, - isFavorite: Boolean(projectFavorites?.includes(w.id)) - })) - .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)) - .slice(offset, limit * page); - - return { - workspacesWithFaveProp: workspacesWithFav - }; - }, [filteredWorkspaces, projectFavorites]); - - const addProjectToFavorites = async (projectId: string) => { - try { - if (currentOrg?.id) { - await updateUserProjectFavorites({ - orgId: currentOrg?.id, - projectFavorites: [...(projectFavorites || []), projectId] - }); - } - } catch (err) { - createNotification({ - text: "Failed to add project to favorites.", - type: "error" - }); + if (router.isReady && currentOrg?.id) { + router.push(`/org/${currentOrg?.id}/${ProjectType.SecretManager}/overview`); } - }; + }, [router.isReady, currentOrg?.id]); - const removeProjectFromFavorites = async (projectId: string) => { - try { - if (currentOrg?.id) { - await updateUserProjectFavorites({ - orgId: currentOrg?.id, - projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] - }); - } - } catch (err) { - createNotification({ - text: "Failed to remove project from favorites.", - type: "error" - }); - } - }; - - const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -
{ - router.push(`/project/${workspace.id}/secrets/overview`); - localStorage.setItem("projectData.id", workspace.id); - }} - key={workspace.id} - className="min-w-72 flex h-40 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4" - > -
-
{workspace.name}
- {isFavorite ? ( - { - e.stopPropagation(); - removeProjectFromFavorites(workspace.id); - }} - /> - ) : ( - { - e.stopPropagation(); - addProjectToFavorites(workspace.id); - }} - /> - )} -
- -
- {workspace.description} -
- -
-
- {workspace.environments?.length || 0} environments -
- -
-
- ); - - const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -
{ - router.push(`/project/${workspace.id}/secrets/overview`); - localStorage.setItem("projectData.id", workspace.id); - }} - key={workspace.id} - className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ - index === 0 && "rounded-t-md" - }`} - > -
-
{workspace.name}
-
-
-
- {workspace.environments?.length || 0} environments -
- {isFavorite ? ( - { - e.stopPropagation(); - removeProjectFromFavorites(workspace.id); - }} - /> - ) : ( - { - e.stopPropagation(); - addProjectToFavorites(workspace.id); - }} - /> - )} -
-
- ); - - let projectsComponents: ReactNode; - - if (filteredWorkspaces.length || isProjectViewLoading) { - switch (projectsViewMode) { - case ProjectsViewMode.GRID: - projectsComponents = ( -
- {isProjectViewLoading && - Array.apply(0, Array(3)).map((_x, i) => ( -
-
- -
-
- -
-
- -
-
- ))} - {!isProjectViewLoading && ( - <> - {workspacesWithFaveProp.map((workspace) => - renderProjectGridItem(workspace, workspace.isFavorite) - )} - - )} -
- ); - - break; - case ProjectsViewMode.LIST: - default: - projectsComponents = ( -
- {isProjectViewLoading && - Array.apply(0, Array(3)).map((_x, i) => ( -
- -
- ))} - {!isProjectViewLoading && - workspacesWithFaveProp.map((workspace, ind) => - renderProjectListItem(workspace, workspace.isFavorite, ind) - )} -
- ); - break; - } - } else if (orgWorkspaces.length) { - projectsComponents = ( -
- -
No projects match search...
-
- ); - } - - return ( -
- - {t("common.head-title", { title: t("settings.members.title") })} - - - {!serverDetails?.redisConfigured && ( -
-

Announcements

-
- - Attention: Updated versions of Infisical now require Redis for full functionality. Learn - how to configure it - - - here - - - . -
-
- )} -
-
-

Projects

-
-
- setSearchFilter(e.target.value)} - leftIcon={} - /> -
- - - - - -
-
- { - localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID); - setProjectsViewMode(ProjectsViewMode.GRID); - }} - ariaLabel="grid" - size="xs" - className={`${ - projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent" - } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} - > - - - { - localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST); - setProjectsViewMode(ProjectsViewMode.LIST); - }} - ariaLabel="list" - size="xs" - className={`${ - projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent" - } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} - > - - -
- - {(isAllowed) => ( - - )} - -
- {projectsComponents} - {!isProjectViewLoading && Boolean(filteredWorkspaces.length) && ( - - )} - {isWorkspaceEmpty && ( -
- -
- You are not part of any projects in this organization yet. When you are, they will - appear here. -
-
- Create a new project, or ask other organization members to give you necessary - permissions. -
-
- )} -
-
-

Explore Infisical

-
- {features.map((feature) => ( -
-
{feature.name}
-
- {feature.description} -
-
-

- Setup time: 20 min -

- - Learn more{" "} - - -
-
- ))} -
-
- {!(new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000) && ( -
-

Onboarding Guide

-
- - {orgWorkspaces.length !== 0 && ( - <> - - - - )} -
- -
-
- {orgWorkspaces.length !== 0 && ( -
-
-
- - {false && ( -
- -
- )} -
-
Inject secrets locally
-
- Replace .env files with a more secure and efficient alternative. -
-
-
-
- About 2 min -
-
- - {false &&
} -
- )} - {orgWorkspaces.length !== 0 && ( - - )} -
- )} - handlePopUpToggle("addNewWs", isOpen)} - /> - handlePopUpToggle("upgradePlan", isOpen)} - text="You have exceeded the number of projects allowed on the free plan." - /> - {/* */} -
- ); + return
; }; Object.assign(OrganizationPage, { requireAuth: true }); diff --git a/frontend/src/pages/org/[id]/secret-manager/overview.tsx b/frontend/src/pages/org/[id]/secret-manager/overview.tsx new file mode 100644 index 000000000..313f7d738 --- /dev/null +++ b/frontend/src/pages/org/[id]/secret-manager/overview.tsx @@ -0,0 +1,530 @@ +// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex + +import { ReactNode, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import Head from "next/head"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons"; +import { + faArrowDownAZ, + faArrowRight, + faArrowUpZA, + faBorderAll, + faExclamationCircle, + faList, + faMagnifyingGlass, + faPlus, + faSearch, + faStar as faSolidStar +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Button, + IconButton, + Input, + Pagination, + Skeleton, + Tooltip, + UpgradePlanModal +} from "@app/components/v2"; +import { NewProjectModal } from "@app/components/v2/projects"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useSubscription +} from "@app/context"; +import { getProjectHomePage } from "@app/helpers/project"; +import { usePagination, useResetPageHelper } from "@app/hooks"; +import { useGetUserWorkspaces } from "@app/hooks/api"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; +// import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; +import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; +import { Workspace } from "@app/hooks/api/types"; +import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; +import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { usePopUp } from "@app/hooks/usePopUp"; + +enum ProjectsViewMode { + GRID = "grid", + LIST = "list" +} + +enum ProjectOrderBy { + Name = "name" +} + +const formatTitle = (type: ProjectType) => { + if (type === ProjectType.SecretManager) return "Secret Management"; + if (type === ProjectType.CertificateManager) return "Cert Management"; + if (type === ProjectType.KMS) return "Key Management"; + return "SSH"; +}; + +const formatDescription = (type: ProjectType) => { + if (type === ProjectType.SecretManager) + return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc."; + if (type === ProjectType.CertificateManager) + return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices."; + if (type === ProjectType.KMS) + return "Centralize the management of keys for cryptographic operations, such as encryption and decryption."; + return "Generate SSH credentials to provide secure and centralized SSH access control for your infrastructure."; +}; + +type Props = { + type: ProjectType; +}; + +// #TODO: Update all the workspaceIds +export const ProductOverview = ({ type }: Props) => { + const { t } = useTranslation(); + + const router = useRouter(); + + const { data: workspaces, isLoading: isWorkspaceLoading } = useGetUserWorkspaces({ type }); + const { currentOrg } = useOrganization(); + const orgWorkspaces = workspaces || []; + const { data: projectFavorites, isLoading: isProjectFavoritesLoading } = + useGetUserProjectFavorites(currentOrg?.id!); + const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); + + const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading; + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "addNewWs", + "upgradePlan" + ] as const); + + const [searchFilter, setSearchFilter] = useState(""); + const { data: serverDetails } = useFetchServerStatus(); + const [projectsViewMode, setProjectsViewMode] = useState( + (localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID + ); + + const { subscription } = useSubscription(); + + const isAddingProjectsAllowed = subscription?.workspaceLimit + ? subscription.workspacesUsed < subscription.workspaceLimit + : true; + + const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0; + + const { + setPage, + perPage, + setPerPage, + page, + offset, + limit, + toggleOrderDirection, + orderDirection + } = usePagination(ProjectOrderBy.Name, { initPerPage: 24 }); + + const filteredWorkspaces = useMemo( + () => + orgWorkspaces + .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())) + .sort((a, b) => + orderDirection === OrderByDirection.ASC + ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + : b.name.toLowerCase().localeCompare(a.name.toLowerCase()) + ), + [searchFilter, orderDirection, orgWorkspaces] + ); + + useResetPageHelper({ + setPage, + offset, + totalCount: filteredWorkspaces.length + }); + + const { workspacesWithFaveProp } = useMemo(() => { + const workspacesWithFav = filteredWorkspaces + .map((w): Workspace & { isFavorite: boolean } => ({ + ...w, + isFavorite: Boolean(projectFavorites?.includes(w.id)) + })) + .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)) + .slice(offset, limit * page); + + return { + workspacesWithFaveProp: workspacesWithFav + }; + }, [filteredWorkspaces, projectFavorites]); + + const addProjectToFavorites = async (projectId: string) => { + try { + if (currentOrg?.id) { + await updateUserProjectFavorites({ + orgId: currentOrg?.id, + projectFavorites: [...(projectFavorites || []), projectId] + }); + } + } catch (err) { + createNotification({ + text: "Failed to add project to favorites.", + type: "error" + }); + } + }; + + const removeProjectFromFavorites = async (projectId: string) => { + try { + if (currentOrg?.id) { + await updateUserProjectFavorites({ + orgId: currentOrg?.id, + projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] + }); + } + } catch (err) { + createNotification({ + text: "Failed to remove project from favorites.", + type: "error" + }); + } + }; + + const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events +
{ + router.push(getProjectHomePage(workspace)); + localStorage.setItem("projectData.id", workspace.id); + }} + key={workspace.id} + className="min-w-72 flex h-40 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4" + > +
+
{workspace.name}
+ {isFavorite ? ( + { + e.stopPropagation(); + removeProjectFromFavorites(workspace.id); + }} + /> + ) : ( + { + e.stopPropagation(); + addProjectToFavorites(workspace.id); + }} + /> + )} +
+ +
+ {workspace.description} +
+ +
+ {type === ProjectType.SecretManager && ( +
+ {workspace.environments?.length || 0} environments +
+ )} + +
+
+ ); + + const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events +
{ + router.push(getProjectHomePage(workspace)); + localStorage.setItem("projectData.id", workspace.id); + }} + key={workspace.id} + className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ + index === 0 && "rounded-t-md" + }`} + > +
+
{workspace.name}
+
+
+
+ {workspace.environments?.length || 0} environments +
+ {isFavorite ? ( + { + e.stopPropagation(); + removeProjectFromFavorites(workspace.id); + }} + /> + ) : ( + { + e.stopPropagation(); + addProjectToFavorites(workspace.id); + }} + /> + )} +
+
+ ); + + let projectsComponents: ReactNode; + + if (filteredWorkspaces.length || isProjectViewLoading) { + switch (projectsViewMode) { + case ProjectsViewMode.GRID: + projectsComponents = ( +
+ {isProjectViewLoading && + Array.apply(0, Array(3)).map((_x, i) => ( +
+
+ +
+
+ +
+
+ +
+
+ ))} + {!isProjectViewLoading && ( + <> + {workspacesWithFaveProp.map((workspace) => + renderProjectGridItem(workspace, workspace.isFavorite) + )} + + )} +
+ ); + + break; + case ProjectsViewMode.LIST: + default: + projectsComponents = ( +
+ {isProjectViewLoading && + Array.apply(0, Array(3)).map((_x, i) => ( +
+ +
+ ))} + {!isProjectViewLoading && + workspacesWithFaveProp.map((workspace, ind) => + renderProjectListItem(workspace, workspace.isFavorite, ind) + )} +
+ ); + break; + } + } else if (orgWorkspaces.length && searchFilter) { + projectsComponents = ( +
+ +
No projects match search...
+
+ ); + } + + return ( +
+ + {t("common.head-title", { title: t("settings.members.title") })} + + + {!serverDetails?.redisConfigured && ( +
+

Announcements

+
+ + Attention: Updated versions of Infisical now require Redis for full functionality. Learn + how to configure it + + + here + + + . +
+
+ )} +
+
+

{formatTitle(type)}

+
+
+

{formatDescription(type)}

+
+
+ setSearchFilter(e.target.value)} + leftIcon={} + /> +
+ + + + + +
+
+ { + localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID); + setProjectsViewMode(ProjectsViewMode.GRID); + }} + ariaLabel="grid" + size="xs" + className={`${ + projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent" + } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} + > + + + { + localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST); + setProjectsViewMode(ProjectsViewMode.LIST); + }} + ariaLabel="list" + size="xs" + className={`${ + projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent" + } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} + > + + +
+ + {(isAllowed) => ( + + )} + +
+ {projectsComponents} + {!isProjectViewLoading && Boolean(filteredWorkspaces.length) && ( + + )} + {isWorkspaceEmpty && ( +
+ +
+ You are not part of any projects in this organization yet. When you are, they will + appear here. +
+
+ Create a new project, or ask other organization members to give you necessary + permissions. +
+
+ )} +
+ handlePopUpToggle("addNewWs", isOpen)} + projectType={type} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You have exceeded the number of projects allowed on the free plan." + /> +
+ ); +}; + +const SecretManagerOverviewPage = () => ; + +Object.assign(SecretManagerOverviewPage, { requireAuth: true }); + +export default SecretManagerOverviewPage; diff --git a/frontend/src/pages/org/[id]/ssh/overview.tsx b/frontend/src/pages/org/[id]/ssh/overview.tsx new file mode 100644 index 000000000..2ab3bb6cc --- /dev/null +++ b/frontend/src/pages/org/[id]/ssh/overview.tsx @@ -0,0 +1,9 @@ +import { ProjectType } from "@app/hooks/api/workspace/types"; + +import { ProductOverview } from "../secret-manager/overview"; + +const SshManagerOverviewPage = () => ; + +Object.assign(SshManagerOverviewPage, { requireAuth: true }); + +export default SshManagerOverviewPage; diff --git a/frontend/src/pages/secret-manager/[id]/allowlist/index.tsx b/frontend/src/pages/secret-manager/[id]/allowlist/index.tsx new file mode 100644 index 000000000..73c25d802 --- /dev/null +++ b/frontend/src/pages/secret-manager/[id]/allowlist/index.tsx @@ -0,0 +1,21 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { IPAllowlistPage } from "@app/views/Project/IPAllowListPage"; + +const ProjectAllowlist = () => { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: t("settings.project.title") })} + + + + + ); +}; + +export default ProjectAllowlist; + +ProjectAllowlist.requireAuth = true; diff --git a/frontend/src/pages/project/[id]/approval/index.tsx b/frontend/src/pages/secret-manager/[id]/approval/index.tsx similarity index 100% rename from frontend/src/pages/project/[id]/approval/index.tsx rename to frontend/src/pages/secret-manager/[id]/approval/index.tsx diff --git a/frontend/src/pages/secret-manager/[id]/identities/[identityId]/index.tsx b/frontend/src/pages/secret-manager/[id]/identities/[identityId]/index.tsx new file mode 100644 index 000000000..ae8b2716a --- /dev/null +++ b/frontend/src/pages/secret-manager/[id]/identities/[identityId]/index.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { IdentityDetailsPage } from "@app/views/Project/IdentityDetailsPage"; + +export default function ProjectIdentityDetailsPage() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +ProjectIdentityDetailsPage.requireAuth = true; diff --git a/frontend/src/pages/secret-manager/[id]/members/[membershipId]/index.tsx b/frontend/src/pages/secret-manager/[id]/members/[membershipId]/index.tsx new file mode 100644 index 000000000..033aa2b15 --- /dev/null +++ b/frontend/src/pages/secret-manager/[id]/members/[membershipId]/index.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage"; + +export default function Page() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +Page.requireAuth = true; diff --git a/frontend/src/pages/secret-manager/[id]/members/index.tsx b/frontend/src/pages/secret-manager/[id]/members/index.tsx new file mode 100644 index 000000000..4bcbb833e --- /dev/null +++ b/frontend/src/pages/secret-manager/[id]/members/index.tsx @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { MembersPage } from "@app/views/Project/MembersPage"; + +export default function WorkspaceMemberSettings() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +WorkspaceMemberSettings.requireAuth = true; diff --git a/frontend/src/pages/secret-manager/[id]/roles/[roleSlug]/index.tsx b/frontend/src/pages/secret-manager/[id]/roles/[roleSlug]/index.tsx new file mode 100644 index 000000000..17c854cee --- /dev/null +++ b/frontend/src/pages/secret-manager/[id]/roles/[roleSlug]/index.tsx @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { RolePage } from "@app/views/Project/RolePage"; + +export default function Role() { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "Project Settings" })} + + + + + ); +} + +Role.requireAuth = true; diff --git a/frontend/src/pages/project/[id]/secret-rotation/index.tsx b/frontend/src/pages/secret-manager/[id]/secret-rotation/index.tsx similarity index 100% rename from frontend/src/pages/project/[id]/secret-rotation/index.tsx rename to frontend/src/pages/secret-manager/[id]/secret-rotation/index.tsx diff --git a/frontend/src/pages/project/[id]/secrets/[env].tsx b/frontend/src/pages/secret-manager/[id]/secrets/[env].tsx similarity index 100% rename from frontend/src/pages/project/[id]/secrets/[env].tsx rename to frontend/src/pages/secret-manager/[id]/secrets/[env].tsx diff --git a/frontend/src/pages/project/[id]/secrets/overview.tsx b/frontend/src/pages/secret-manager/[id]/secrets/overview.tsx similarity index 100% rename from frontend/src/pages/project/[id]/secrets/overview.tsx rename to frontend/src/pages/secret-manager/[id]/secrets/overview.tsx diff --git a/frontend/src/pages/secret-manager/[id]/settings/index.tsx b/frontend/src/pages/secret-manager/[id]/settings/index.tsx new file mode 100644 index 000000000..331ba8cc5 --- /dev/null +++ b/frontend/src/pages/secret-manager/[id]/settings/index.tsx @@ -0,0 +1,22 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { ProjectSettingsPage } from "@app/views/Settings/ProjectSettingsPage"; + +const ProjectSettings = () => { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.project.title") })} + + + + + ); +}; + +export default ProjectSettings; + +ProjectSettings.requireAuth = true; diff --git a/frontend/src/pages/signup/index.tsx b/frontend/src/pages/signup/index.tsx index 0719111d6..fa26437f4 100644 --- a/frontend/src/pages/signup/index.tsx +++ b/frontend/src/pages/signup/index.tsx @@ -16,6 +16,7 @@ import { useServerConfig } from "@app/context"; import { useVerifySignupEmailVerificationCode } from "@app/hooks/api"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; +import { ProjectType } from "@app/hooks/api/workspace/types"; /** * @returns the signup page @@ -47,7 +48,7 @@ export default function SignUp() { const tryAuth = async () => { try { const userOrgs = await fetchOrganizations(); - router.push(`/org/${userOrgs[0].id}/overview`); + router.push(`/org/${userOrgs[0].id}/${ProjectType.SecretManager}/overview`); } catch (error) { console.log("Error - Not logged in yet"); } @@ -90,7 +91,7 @@ export default function SignUp() { if (!serverDetails?.emailConfigured && step === 5) { const userOrgs = await fetchOrganizations(); - router.push(`/org/${userOrgs[0].id}/overview`); + router.push(`/org/${userOrgs[0].id}/${ProjectType.SecretManager}/overview`); } })(); }, [step]); diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index e64f4d7c0..8a0d712e3 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -31,6 +31,7 @@ import { } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; import { Mfa } from "@app/views/Login/Mfa"; @@ -386,7 +387,7 @@ export default function SignupInvite() { setBackupKeyError, setBackupKeyIssued }); - router.push(`/org/${organizationId}/overview`); + router.push(`/org/${organizationId}/${ProjectType.SecretManager}/overview`); }} size="lg" /> diff --git a/frontend/src/pages/ssh/[id]/allowlist/index.tsx b/frontend/src/pages/ssh/[id]/allowlist/index.tsx new file mode 100644 index 000000000..73c25d802 --- /dev/null +++ b/frontend/src/pages/ssh/[id]/allowlist/index.tsx @@ -0,0 +1,21 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { IPAllowlistPage } from "@app/views/Project/IPAllowListPage"; + +const ProjectAllowlist = () => { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: t("settings.project.title") })} + + + + + ); +}; + +export default ProjectAllowlist; + +ProjectAllowlist.requireAuth = true; diff --git a/frontend/src/pages/project/[id]/ssh/ca/[caId]/index.tsx b/frontend/src/pages/ssh/[id]/ca/[caId]/index.tsx similarity index 100% rename from frontend/src/pages/project/[id]/ssh/ca/[caId]/index.tsx rename to frontend/src/pages/ssh/[id]/ca/[caId]/index.tsx diff --git a/frontend/src/pages/ssh/[id]/identities/[identityId]/index.tsx b/frontend/src/pages/ssh/[id]/identities/[identityId]/index.tsx new file mode 100644 index 000000000..ae8b2716a --- /dev/null +++ b/frontend/src/pages/ssh/[id]/identities/[identityId]/index.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { IdentityDetailsPage } from "@app/views/Project/IdentityDetailsPage"; + +export default function ProjectIdentityDetailsPage() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +ProjectIdentityDetailsPage.requireAuth = true; diff --git a/frontend/src/pages/ssh/[id]/members/[membershipId]/index.tsx b/frontend/src/pages/ssh/[id]/members/[membershipId]/index.tsx new file mode 100644 index 000000000..033aa2b15 --- /dev/null +++ b/frontend/src/pages/ssh/[id]/members/[membershipId]/index.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage"; + +export default function Page() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +Page.requireAuth = true; diff --git a/frontend/src/pages/ssh/[id]/members/index.tsx b/frontend/src/pages/ssh/[id]/members/index.tsx new file mode 100644 index 000000000..4bcbb833e --- /dev/null +++ b/frontend/src/pages/ssh/[id]/members/index.tsx @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { MembersPage } from "@app/views/Project/MembersPage"; + +export default function WorkspaceMemberSettings() { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + ); +} + +WorkspaceMemberSettings.requireAuth = true; diff --git a/frontend/src/pages/ssh/[id]/roles/[roleSlug]/index.tsx b/frontend/src/pages/ssh/[id]/roles/[roleSlug]/index.tsx new file mode 100644 index 000000000..17c854cee --- /dev/null +++ b/frontend/src/pages/ssh/[id]/roles/[roleSlug]/index.tsx @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { RolePage } from "@app/views/Project/RolePage"; + +export default function Role() { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "Project Settings" })} + + + + + ); +} + +Role.requireAuth = true; diff --git a/frontend/src/pages/ssh/[id]/settings/index.tsx b/frontend/src/pages/ssh/[id]/settings/index.tsx new file mode 100644 index 000000000..331ba8cc5 --- /dev/null +++ b/frontend/src/pages/ssh/[id]/settings/index.tsx @@ -0,0 +1,22 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { ProjectSettingsPage } from "@app/views/Settings/ProjectSettingsPage"; + +const ProjectSettings = () => { + const { t } = useTranslation(); + + return ( + <> + + {t("common.head-title", { title: t("settings.project.title") })} + + + + + ); +}; + +export default ProjectSettings; + +ProjectSettings.requireAuth = true; diff --git a/frontend/src/pages/project/[id]/ssh/index.tsx b/frontend/src/pages/ssh/[id]/ssh/index.tsx similarity index 100% rename from frontend/src/pages/project/[id]/ssh/index.tsx rename to frontend/src/pages/ssh/[id]/ssh/index.tsx diff --git a/frontend/src/views/Login/Login.utils.tsx b/frontend/src/views/Login/Login.utils.tsx index b0cd3d031..dc714e4b0 100644 --- a/frontend/src/views/Login/Login.utils.tsx +++ b/frontend/src/views/Login/Login.utils.tsx @@ -3,6 +3,7 @@ import { NextRouter, useRouter } from "next/router"; import { useServerConfig } from "@app/context"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { userKeys } from "@app/hooks/api/users"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { queryClient } from "@app/reactQuery"; export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => { @@ -12,7 +13,7 @@ export const navigateUserToOrg = async (router: NextRouter, organizationId?: str if (organizationId) { localStorage.setItem("orgData.id", organizationId); - router.push(`/org/${organizationId}/overview`); + router.push(`/org/${organizationId}/${ProjectType.SecretManager}/overview`); return; } @@ -20,7 +21,7 @@ export const navigateUserToOrg = async (router: NextRouter, organizationId?: str // user is part of at least 1 non-auth enforced org const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id; localStorage.setItem("orgData.id", userOrg); - router.push(`/org/${userOrg}/overview`); + router.push(`/org/${userOrg}/${ProjectType.SecretManager}/overview`); } else { // user is not part of any non-auth enforced orgs localStorage.removeItem("orgData.id"); diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx index 4cbd1362d..a6342dbaf 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectRow.tsx @@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; import { createNotification } from "@app/components/notifications"; -import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; +import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { IdentityMembership } from "@app/hooks/api/identities/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; @@ -52,7 +52,9 @@ export const IdentityProjectRow = ({ key={`identity-project-membership-${id}`} onClick={() => { if (isAccessible) { - router.push(`/project/${project.id}/members?selectedTab=${TabSections.Identities}`); + router.push( + `/${project?.type}/${project.id}/members?selectedTab=${TabSections.Identities}` + ); return; } @@ -63,6 +65,9 @@ export const IdentityProjectRow = ({ }} > {project.name} + + {project.type} + {`${formatRoleName(roles[0].role, roles[0].customRoleName)}${ roles.length > 1 ? ` (+${roles.length - 1})` : "" }`} diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx index 195f74111..cb9f1c640 100644 --- a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx +++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx @@ -106,6 +106,7 @@ export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) =>
+ Type Role Added On diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx index 8852af872..fe03e5e68 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx @@ -23,12 +23,17 @@ import { useDeleteIdentityTokenAuth, useDeleteIdentityUniversalAuth } from "@app/hooks/api"; -import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; +import { + IdentityAuthMethod, + identityAuthToNameMap, + useDeleteIdentityJwtAuth +} from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm"; import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm"; +import { IdentityJwtAuthForm } from "./IdentityJwtAuthForm"; import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm"; import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm"; import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm"; @@ -68,7 +73,11 @@ const identityAuthMethods = [ { label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH }, { label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }, { label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH }, - { label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH } + { label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }, + { + label: "JWT Auth", + value: IdentityAuthMethod.JWT_AUTH + } ]; const schema = yup @@ -100,6 +109,7 @@ export const IdentityAuthMethodModalContent = ({ const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth(); const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); + const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth(); const { control, watch } = useForm({ resolver: yupResolver(schema), @@ -216,6 +226,17 @@ export const IdentityAuthMethodModalContent = ({ handlePopUpToggle={handlePopUpToggle} /> ) + }, + + [IdentityAuthMethod.JWT_AUTH]: { + revokeMethod: revokeJwtAuth, + render: () => ( + + ) } }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx new file mode 100644 index 000000000..d31bd43bd --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx @@ -0,0 +1,688 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; +import { faPlus, 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, + Select, + SelectItem, + TextArea, + Tooltip +} from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { useAddIdentityJwtAuth, useUpdateIdentityJwtAuth } from "@app/hooks/api"; +import { IdentityAuthMethod } from "@app/hooks/api/identities"; +import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums"; +import { useGetIdentityJwtAuth } from "@app/hooks/api/identities/queries"; +import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const commonSchema = z.object({ + accessTokenTrustedIps: z + .array( + z.object({ + ipAddress: z.string().max(50) + }) + ) + .min(1), + 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(), + boundIssuer: z.string().trim().default(""), + boundAudiences: z.string().optional().default(""), + boundClaims: z.array( + z.object({ + key: z.string(), + value: z.string() + }) + ), + boundSubject: z.string().optional().default("") +}); + +const schema = z.discriminatedUnion("configurationType", [ + z + .object({ + configurationType: z.literal(IdentityJwtConfigurationType.JWKS), + jwksUrl: z.string().trim().url(), + jwksCaCert: z.string().trim().default(""), + publicKeys: z + .object({ + value: z.string() + }) + .array() + .optional() + }) + .merge(commonSchema), + z + .object({ + configurationType: z.literal(IdentityJwtConfigurationType.STATIC), + jwksUrl: z.string().trim().optional(), + jwksCaCert: z.string().trim().optional().default(""), + publicKeys: z + .object({ + value: z.string().min(1) + }) + .array() + .min(1) + }) + .merge(commonSchema) +]); + +export type FormData = z.infer; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, + state?: boolean + ) => void; + identityAuthMethodData: { + identityId: string; + name: string; + configuredAuthMethods?: IdentityAuthMethod[]; + authMethod?: IdentityAuthMethod; + }; +}; + +export const IdentityJwtAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth(); + + const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes( + identityAuthMethodData.authMethod! || "" + ); + const { data } = useGetIdentityJwtAuth(identityAuthMethodData?.identityId ?? "", { + enabled: isUpdate + }); + + const { + watch, + control, + handleSubmit, + reset, + setValue, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + configurationType: IdentityJwtConfigurationType.JWKS + } + }); + + const selectedConfigurationType = watch("configurationType") as IdentityJwtConfigurationType; + + const { + fields: publicKeyFields, + append: appendPublicKeyFields, + remove: removePublicKeyFields + } = useFieldArray({ + control, + name: "publicKeys" + }); + + const { + fields: boundClaimsFields, + append: appendBoundClaimField, + remove: removeBoundClaimField + } = useFieldArray({ + control, + name: "boundClaims" + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + configurationType: data.configurationType, + jwksUrl: data.jwksUrl, + jwksCaCert: data.jwksCaCert, + publicKeys: data.publicKeys.map((pk) => ({ + value: pk + })), + boundIssuer: data.boundIssuer, + boundAudiences: data.boundAudiences, + boundClaims: Object.entries(data.boundClaims).map(([key, value]) => ({ + key, + value + })), + boundSubject: data.boundSubject, + 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({ + configurationType: IdentityJwtConfigurationType.JWKS, + jwksUrl: "", + jwksCaCert: "", + boundIssuer: "", + boundAudiences: "", + boundClaims: [], + boundSubject: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + accessTokenTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys, + boundIssuer, + boundAudiences, + boundClaims, + boundSubject + }: FormData) => { + try { + if (!identityAuthMethodData) { + return; + } + + if (data) { + await updateMutateAsync({ + identityId: identityAuthMethodData.identityId, + organizationId: orgId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys: publicKeys?.map((field) => field.value).filter(Boolean), + boundIssuer, + boundAudiences, + boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), + boundSubject, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + identityId: identityAuthMethodData.identityId, + configurationType, + jwksUrl, + jwksCaCert, + publicKeys: publicKeys?.map((field) => field.value).filter(Boolean), + boundIssuer, + boundAudiences, + boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])), + boundSubject, + organizationId: orgId, + 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 (err) { + createNotification({ + text: `Failed to ${isUpdate ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + {selectedConfigurationType === IdentityJwtConfigurationType.JWKS && ( + <> + ( + + + + )} + /> + ( + +