diff --git a/.infisicalignore b/.infisicalignore index b00bf0995..02cdd4f0e 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -28,3 +28,15 @@ frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow docs/cli/commands/user.mdx:generic-api-key:51 frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76 docs/integrations/app-connections/hashicorp-vault.mdx:generic-api-key:188 +cli/detect/config/gitleaks.toml:gcp-api-key:567 +cli/detect/config/gitleaks.toml:gcp-api-key:569 +cli/detect/config/gitleaks.toml:gcp-api-key:570 +cli/detect/config/gitleaks.toml:gcp-api-key:572 +cli/detect/config/gitleaks.toml:gcp-api-key:574 +cli/detect/config/gitleaks.toml:gcp-api-key:575 +cli/detect/config/gitleaks.toml:gcp-api-key:576 +cli/detect/config/gitleaks.toml:gcp-api-key:577 +cli/detect/config/gitleaks.toml:gcp-api-key:578 +cli/detect/config/gitleaks.toml:gcp-api-key:579 +cli/detect/config/gitleaks.toml:gcp-api-key:581 +cli/detect/config/gitleaks.toml:gcp-api-key:582 diff --git a/backend/package.json b/backend/package.json index f226439d3..30aa9f68c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,8 +38,8 @@ "build:frontend": "npm run build --prefix ../frontend", "start": "node --enable-source-maps dist/main.mjs", "type:check": "tsc --noEmit", - "lint:fix": "eslint --fix --ext js,ts ./src", - "lint": "eslint 'src/**/*.ts'", + "lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src", + "lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'", "test:unit": "vitest run -c vitest.unit.config.ts", "test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1", "test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 748a7d431..8e70bdae4 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -80,6 +80,7 @@ import { TOrgServiceFactory } from "@app/services/org/org-service"; import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; +import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service"; import { TProjectServiceFactory } from "@app/services/project/project-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; @@ -232,6 +233,7 @@ declare module "fastify" { certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; certificateEst: TCertificateEstServiceFactory; pkiCollection: TPkiCollectionServiceFactory; + pkiSubscriber: TPkiSubscriberServiceFactory; secretScanning: TSecretScanningServiceFactory; license: TLicenseServiceFactory; trustedIp: TTrustedIpServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index c26f1128e..4f136bef0 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -209,6 +209,9 @@ import { TPkiCollections, TPkiCollectionsInsert, TPkiCollectionsUpdate, + TPkiSubscribers, + TPkiSubscribersInsert, + TPkiSubscribersUpdate, TProjectBots, TProjectBotsInsert, TProjectBotsUpdate, @@ -564,6 +567,11 @@ declare module "knex/types/tables" { TPkiCollectionItemsInsert, TPkiCollectionItemsUpdate >; + [TableName.PkiSubscriber]: KnexOriginal.CompositeTableType< + TPkiSubscribers, + TPkiSubscribersInsert, + TPkiSubscribersUpdate + >; [TableName.UserGroupMembership]: KnexOriginal.CompositeTableType< TUserGroupMembership, TUserGroupMembershipInsert, diff --git a/backend/src/db/migrations/20250508160957_pki-subscriber.ts b/backend/src/db/migrations/20250508160957_pki-subscriber.ts new file mode 100644 index 000000000..0e1b50f03 --- /dev/null +++ b/backend/src/db/migrations/20250508160957_pki-subscriber.ts @@ -0,0 +1,46 @@ +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.PkiSubscriber))) { + await knex.schema.createTable(TableName.PkiSubscriber, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("caId").nullable(); + t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("SET NULL"); + t.string("name").notNullable(); + t.string("commonName").notNullable(); + t.specificType("subjectAlternativeNames", "text[]").notNullable(); + t.string("ttl").notNullable(); + t.specificType("keyUsages", "text[]").notNullable(); + t.specificType("extendedKeyUsages", "text[]").notNullable(); + t.string("status").notNullable(); // active / disabled + t.unique(["projectId", "name"]); + }); + await createOnUpdateTrigger(knex, TableName.PkiSubscriber); + } + + const hasSubscriberCol = await knex.schema.hasColumn(TableName.Certificate, "pkiSubscriberId"); + if (!hasSubscriberCol) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.uuid("pkiSubscriberId").nullable(); + t.foreign("pkiSubscriberId").references("id").inTable(TableName.PkiSubscriber).onDelete("SET NULL"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasSubscriberCol = await knex.schema.hasColumn(TableName.Certificate, "pkiSubscriberId"); + if (hasSubscriberCol) { + await knex.schema.alterTable(TableName.Certificate, (t) => { + t.dropColumn("pkiSubscriberId"); + }); + } + + await knex.schema.dropTableIfExists(TableName.PkiSubscriber); + await dropOnUpdateTrigger(knex, TableName.PkiSubscriber); +} diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index 533f9b898..cbd4f64f9 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -24,7 +24,8 @@ export const CertificatesSchema = z.object({ caCertId: z.string().uuid(), certificateTemplateId: z.string().uuid().nullable().optional(), keyUsages: z.string().array().nullable().optional(), - extendedKeyUsages: z.string().array().nullable().optional() + extendedKeyUsages: z.string().array().nullable().optional(), + pkiSubscriberId: z.string().uuid().nullable().optional() }); export type TCertificates = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index b71d51908..ebbe417c4 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -69,6 +69,7 @@ export * from "./organizations"; export * from "./pki-alerts"; export * from "./pki-collection-items"; export * from "./pki-collections"; +export * from "./pki-subscribers"; export * from "./project-bots"; export * from "./project-environments"; export * from "./project-gateways"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 81d5319e1..912c8ac46 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -21,6 +21,7 @@ export enum TableName { CertificateBody = "certificate_bodies", CertificateSecret = "certificate_secrets", CertificateTemplate = "certificate_templates", + PkiSubscriber = "pki_subscribers", PkiAlert = "pki_alerts", PkiCollection = "pki_collections", PkiCollectionItem = "pki_collection_items", diff --git a/backend/src/db/schemas/pki-subscribers.ts b/backend/src/db/schemas/pki-subscribers.ts new file mode 100644 index 000000000..08db19806 --- /dev/null +++ b/backend/src/db/schemas/pki-subscribers.ts @@ -0,0 +1,27 @@ +// 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 PkiSubscribersSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + projectId: z.string(), + caId: z.string().uuid().nullable().optional(), + name: z.string(), + commonName: z.string(), + subjectAlternativeNames: z.string().array(), + ttl: z.string(), + keyUsages: z.string().array(), + extendedKeyUsages: z.string().array(), + status: z.string() +}); + +export type TPkiSubscribers = z.infer; +export type TPkiSubscribersInsert = Omit, TImmutableDBKeys>; +export type TPkiSubscribersUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/ee/routes/v1/ssh-host-router.ts b/backend/src/ee/routes/v1/ssh-host-router.ts index 93748c27f..4c749f6f5 100644 --- a/backend/src/ee/routes/v1/ssh-host-router.ts +++ b/backend/src/ee/routes/v1/ssh-host-router.ts @@ -73,7 +73,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const host = await server.services.sshHost.getSshHost({ + const host = await server.services.sshHost.getSshHostById({ sshHostId: req.params.sshHostId, actor: req.permission.type, actorId: req.permission.id, 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 126fd2323..03f11219e 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -19,7 +19,7 @@ import { TProjectPermission } from "@app/lib/types"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types"; import { ActorType } from "@app/services/auth/auth-type"; -import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; @@ -254,6 +254,13 @@ export enum EventType { GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items", ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item", DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item", + CREATE_PKI_SUBSCRIBER = "create-pki-subscriber", + UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber", + DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber", + GET_PKI_SUBSCRIBER = "get-pki-subscriber", + ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert", + SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert", + LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs", CREATE_KMS = "create-kms", UPDATE_KMS = "update-kms", DELETE_KMS = "delete-kms", @@ -1965,6 +1972,77 @@ interface DeletePkiCollectionItem { }; } +interface CreatePkiSubscriber { + type: EventType.CREATE_PKI_SUBSCRIBER; + metadata: { + pkiSubscriberId: string; + caId?: string; + name: string; + commonName: string; + ttl: string; + subjectAlternativeNames: string[]; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; + }; +} + +interface UpdatePkiSubscriber { + type: EventType.UPDATE_PKI_SUBSCRIBER; + metadata: { + pkiSubscriberId: string; + caId?: string; + name?: string; + commonName?: string; + ttl?: string; + subjectAlternativeNames?: string[]; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; + }; +} + +interface DeletePkiSubscriber { + type: EventType.DELETE_PKI_SUBSCRIBER; + metadata: { + pkiSubscriberId: string; + name: string; + }; +} + +interface GetPkiSubscriber { + type: EventType.GET_PKI_SUBSCRIBER; + metadata: { + pkiSubscriberId: string; + name: string; + }; +} + +interface IssuePkiSubscriberCert { + type: EventType.ISSUE_PKI_SUBSCRIBER_CERT; + metadata: { + subscriberId: string; + name: string; + serialNumber: string; + }; +} + +interface SignPkiSubscriberCert { + type: EventType.SIGN_PKI_SUBSCRIBER_CERT; + metadata: { + subscriberId: string; + name: string; + serialNumber: string; + }; +} + +interface ListPkiSubscriberCerts { + type: EventType.LIST_PKI_SUBSCRIBER_CERTS; + metadata: { + subscriberId: string; + name: string; + projectId: string; + }; +} + interface CreateKmsEvent { type: EventType.CREATE_KMS; metadata: { @@ -2928,6 +3006,13 @@ export type Event = | GetPkiCollectionItems | AddPkiCollectionItem | DeletePkiCollectionItem + | CreatePkiSubscriber + | UpdatePkiSubscriber + | DeletePkiSubscriber + | GetPkiSubscriber + | IssuePkiSubscriberCert + | SignPkiSubscriberCert + | ListPkiSubscriberCerts | CreateKmsEvent | UpdateKmsEvent | DeleteKmsEvent diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 44fcb7825..a3c9c2c11 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -9,6 +9,7 @@ import { ProjectPermissionIdentityActions, ProjectPermissionKmipActions, ProjectPermissionMemberActions, + ProjectPermissionPkiSubscriberActions, ProjectPermissionSecretActions, ProjectPermissionSecretRotationActions, ProjectPermissionSecretSyncActions, @@ -76,6 +77,18 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.SshHosts ); + can( + [ + ProjectPermissionPkiSubscriberActions.Edit, + ProjectPermissionPkiSubscriberActions.Read, + ProjectPermissionPkiSubscriberActions.Create, + ProjectPermissionPkiSubscriberActions.Delete, + ProjectPermissionPkiSubscriberActions.IssueCert, + ProjectPermissionPkiSubscriberActions.ListCerts + ], + ProjectPermissionSub.PkiSubscribers + ); + can( [ ProjectPermissionMemberActions.Create, @@ -338,6 +351,7 @@ const buildMemberPermissionRules = () => { can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts); + can([ProjectPermissionPkiSubscriberActions.Read], ProjectPermissionSub.PkiSubscribers); can( [ diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 7463c35f6..5474facf6 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -87,6 +87,15 @@ export enum ProjectPermissionSshHostActions { IssueHostCert = "issue-host-cert" } +export enum ProjectPermissionPkiSubscriberActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + IssueCert = "issue-cert", + ListCerts = "list-certs" +} + export enum ProjectPermissionSecretSyncActions { Read = "read", Create = "create", @@ -143,6 +152,7 @@ export enum ProjectPermissionSub { SshCertificateTemplates = "ssh-certificate-templates", SshHosts = "ssh-hosts", SshHostGroups = "ssh-host-groups", + PkiSubscribers = "pki-subscribers", PkiAlerts = "pki-alerts", PkiCollections = "pki-collections", Kms = "kms", @@ -190,6 +200,11 @@ export type SshHostSubjectFields = { hostname: string; }; +export type PkiSubscriberSubjectFields = { + name: string; + // (dangtony98): consider adding [commonName] as a subject field in the future +}; + export type ProjectPermissionSet = | [ ProjectPermissionSecretActions, @@ -249,6 +264,13 @@ export type ProjectPermissionSet = ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts | (ForcedSubject & SshHostSubjectFields) ] + | [ + ProjectPermissionPkiSubscriberActions, + ( + | ProjectPermissionSub.PkiSubscribers + | (ForcedSubject & PkiSubscriberSubjectFields) + ) + ] | [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] @@ -399,6 +421,21 @@ const SshHostConditionSchema = z }) .partial(); +const PkiSubscriberConditionSchema = z + .object({ + name: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + ]) + }) + .partial(); + const GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -663,6 +700,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ "When specified, only matching conditions will be allowed to access given resource." ).optional() }), + z.object({ + subject: z.literal(ProjectPermissionSub.PkiSubscribers).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPkiSubscriberActions).describe( + "Describe what action an entity can take." + ), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + conditions: PkiSubscriberConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), z.object({ subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."), diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index be4a7ab3b..3877cbaf8 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -334,7 +334,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecret).as("commitSecretId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecret).as("commitId"), db.raw( - `DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank` + `DENSE_RANK() OVER (PARTITION BY ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."createdAt" DESC) as rank` ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), @@ -483,7 +483,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitSecretId"), db.ref("id").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitId"), db.raw( - `DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank` + `DENSE_RANK() OVER (PARTITION BY ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."createdAt" DESC) as rank` ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), db.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), diff --git a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts index 1eacc7602..1137660d6 100644 --- a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts +++ b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts @@ -186,13 +186,42 @@ export const sshHostGroupServiceFactory = ({ }); const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => { - await sshHostGroupDAL.updateById( - sshHostGroupId, - { - name - }, - tx - ); + if (name && name !== sshHostGroup.name) { + // (dangtony98): room to optimize check to ensure that + // the SSH host group name is unique across the whole org + const project = await projectDAL.findById(sshHostGroup.projectId, tx); + if (!project) throw new NotFoundError({ message: `Project with ID '${sshHostGroup.projectId}' not found` }); + const projects = await projectDAL.find( + { + orgId: project.orgId + }, + { tx } + ); + + const existingSshHostGroup = await sshHostGroupDAL.find( + { + name, + $in: { + projectId: projects.map((p) => p.id) + } + }, + { tx } + ); + + if (existingSshHostGroup.length) { + throw new BadRequestError({ + message: `SSH host group with name '${name}' already exists in the organization` + }); + } + await sshHostGroupDAL.updateById( + sshHostGroupId, + { + name + }, + tx + ); + } + if (loginMappings) { await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx); if (loginMappings.length) { diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index 79c74cd57..e41a8b403 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -335,7 +335,7 @@ export const sshHostServiceFactory = ({ return host; }; - const getSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => { + const getSshHostById = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => { const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId); if (!host) { throw new NotFoundError({ @@ -631,7 +631,7 @@ export const sshHostServiceFactory = ({ createSshHost, updateSshHost, deleteSshHost, - getSshHost, + getSshHostById, issueSshHostUserCert, issueSshHostHostCert, getSshHostUserCaPk, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b3c64c7c1..a4556a75d 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -46,6 +46,7 @@ export enum ApiDocsTags { PkiCertificateTemplates = "PKI Certificate Templates", PkiCertificateCollections = "PKI Certificate Collections", PkiAlerting = "PKI Alerting", + PkiSubscribers = "PKI Subscribers", SshCertificates = "SSH Certificates", SshCertificateAuthorities = "SSH Certificate Authorities", SshCertificateTemplates = "SSH Certificate Templates", @@ -639,6 +640,9 @@ export const PROJECTS = { commonName: "The common name of the certificate to filter by.", offset: "The offset to start from. If you enter 10, it will start from the 10th certificate.", limit: "The number of certificates to return." + }, + LIST_PKI_SUBSCRIBERS: { + projectId: "The ID of the project to list PKI subscribers for." } } as const; @@ -1731,6 +1735,67 @@ export const ALERTS = { } }; +export const PKI_SUBSCRIBERS = { + GET: { + subscriberName: "The name of the PKI subscriber to get.", + projectId: "The ID of the project to get the PKI subscriber for." + }, + CREATE: { + projectId: "The ID of the project to create the PKI subscriber in.", + caId: "The ID of the CA that will issue certificates for the PKI subscriber.", + name: "The name of the PKI subscriber.", + commonName: "The common name (CN) to be used on certificates issued for this subscriber.", + status: "The status of the PKI subscriber. This can be one of active or disabled.", + ttl: "The time to live for the certificates issued for this subscriber such as 1m, 1h, 1d, 1y, ...", + subjectAlternativeNames: + "A list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.", + keyUsages: "The key usage extension to be used on certificates issued for this subscriber.", + extendedKeyUsages: "The extended key usage extension to be used on certificates issued for this subscriber." + }, + UPDATE: { + projectId: "The ID of the project to update the PKI subscriber in.", + subscriberName: "The name of the PKI subscriber to update.", + caId: "The ID of the CA that will issue certificates for the PKI subscriber to update to.", + name: "The name of the PKI subscriber to update to.", + commonName: "The common name (CN) to be used on certificates issued for this subscriber to update to.", + status: "The status of the PKI subscriber to update to. This can be one of active or disabled.", + ttl: "The time to live for the certificates issued for this subscriber such as 1m, 1h, 1d, 1y, ...", + subjectAlternativeNames: + "A comma-delimited list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.", + keyUsages: "The key usage extension to be used on certificates issued for this subscriber to update to.", + extendedKeyUsages: + "The extended key usage extension to be used on certificates issued for this subscriber to update to." + }, + DELETE: { + subscriberName: "The name of the PKI subscriber to delete.", + projectId: "The ID of the project of the PKI subscriber to delete." + }, + ISSUE_CERT: { + subscriberName: "The name of the PKI subscriber to issue the certificate for.", + projectId: "The ID of the project of the PKI subscriber to issue the certificate for.", + certificate: "The issued certificate.", + issuingCaCertificate: "The certificate of the issuing CA.", + certificateChain: "The certificate chain of the issued certificate.", + privateKey: "The private key of the issued certificate.", + serialNumber: "The serial number of the issued certificate." + }, + SIGN_CERT: { + subscriberName: "The name of the PKI subscriber to sign the certificate for.", + projectId: "The ID of the project of the PKI subscriber to sign the certificate for.", + csr: "The CSR to be used to sign the certificate.", + certificate: "The signed certificate.", + issuingCaCertificate: "The certificate of the issuing CA.", + certificateChain: "The certificate chain of the signed certificate.", + serialNumber: "The serial number of the signed certificate." + }, + LIST_CERTS: { + subscriberName: "The name of the PKI subscriber to list the certificates for.", + projectId: "The ID of the project of the PKI subscriber to list the certificates for.", + offset: "The offset to start from.", + limit: "The number of certificates to return." + } +}; + export const PKI_COLLECTIONS = { CREATE: { projectId: "The ID of the project to create the PKI collection in.", diff --git a/backend/src/server/plugins/serve-ui.ts b/backend/src/server/plugins/serve-ui.ts index 9f91d9774..22c097726 100644 --- a/backend/src/server/plugins/serve-ui.ts +++ b/backend/src/server/plugins/serve-ui.ts @@ -57,7 +57,9 @@ export const registerServeUI = async ( reply.callNotFound(); return; } - return reply.sendFile("index.html"); + // reference: https://github.com/fastify/fastify-static?tab=readme-ov-file#managing-cache-control-headers + // to avoid ui bundle skew on new deployment + return reply.sendFile("index.html", { maxAge: 0, immutable: false }); } }); } diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index cb9931ec2..844f6c31e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -197,6 +197,8 @@ import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-servic import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal"; import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; +import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; +import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service"; import { projectDALFactory } from "@app/services/project/project-dal"; import { projectQueueFactory } from "@app/services/project/project-queue"; import { projectServiceFactory } from "@app/services/project/project-service"; @@ -828,6 +830,7 @@ export const registerRoutes = async ( const pkiAlertDAL = pkiAlertDALFactory(db); const pkiCollectionDAL = pkiCollectionDALFactory(db); const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db); + const pkiSubscriberDAL = pkiSubscriberDALFactory(db); const certificateService = certificateServiceFactory({ certificateDAL, @@ -962,6 +965,20 @@ export const registerRoutes = async ( projectDAL }); + const pkiSubscriberService = pkiSubscriberServiceFactory({ + pkiSubscriberDAL, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + certificateAuthoritySecretDAL, + certificateAuthorityCrlDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + projectDAL, + kmsService, + permissionService + }); + const projectTemplateService = projectTemplateServiceFactory({ licenseService, permissionService, @@ -1059,6 +1076,7 @@ export const registerRoutes = async ( projectRoleDAL, folderDAL, licenseService, + pkiSubscriberDAL, certificateAuthorityDAL, certificateDAL, pkiAlertDAL, @@ -1745,6 +1763,7 @@ export const registerRoutes = async ( certificateEst: certificateEstService, pkiAlert: pkiAlertService, pkiCollection: pkiCollectionService, + pkiSubscriber: pkiSubscriberService, secretScanning: secretScanningService, license: licenseService, trustedIp: trustedIpService, diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index b1a49f815..7950b9efe 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -33,6 +33,7 @@ import { registerOrgRouter } from "./organization-router"; import { registerPasswordRouter } from "./password-router"; import { registerPkiAlertRouter } from "./pki-alert-router"; import { registerPkiCollectionRouter } from "./pki-collection-router"; +import { registerPkiSubscriberRouter } from "./pki-subscriber-router"; import { registerProjectEnvRouter } from "./project-env-router"; import { registerProjectKeyRouter } from "./project-key-router"; import { registerProjectMembershipRouter } from "./project-membership-router"; @@ -105,6 +106,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" }); await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" }); await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" }); + await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" }); }, { prefix: "/pki" } ); diff --git a/backend/src/server/routes/v1/pki-subscriber-router.ts b/backend/src/server/routes/v1/pki-subscriber-router.ts new file mode 100644 index 000000000..d04b8b4bb --- /dev/null +++ b/backend/src/server/routes/v1/pki-subscriber-router.ts @@ -0,0 +1,478 @@ +import { z } from "zod"; + +import { CertificatesSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, PKI_SUBSCRIBERS } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { validateAltNameField } from "@app/services/certificate-authority/certificate-authority-validators"; +import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema"; +import { PkiSubscriberStatus } from "@app/services/pki-subscriber/pki-subscriber-types"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + +export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:subscriberName", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Get PKI Subscriber", + params: z.object({ + subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET.subscriberName) + }), + querystring: z.object({ + projectId: z.string().describe(PKI_SUBSCRIBERS.GET.projectId) + }), + response: { + 200: sanitizedPkiSubscriber + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const subscriber = await server.services.pkiSubscriber.getSubscriber({ + subscriberName: req.params.subscriberName, + projectId: req.query.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.GET_PKI_SUBSCRIBER, + metadata: { + pkiSubscriberId: subscriber.id, + name: subscriber.name + } + } + }); + + return subscriber; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Create PKI Subscriber", + body: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.CREATE.projectId), + caId: z + .string() + .trim() + .uuid("CA ID must be a valid UUID") + .min(1, "CA ID is required") + .describe(PKI_SUBSCRIBERS.CREATE.caId), + name: slugSchema({ min: 1, max: 64, field: "name" }).describe(PKI_SUBSCRIBERS.CREATE.name), + commonName: z.string().trim().min(1).describe(PKI_SUBSCRIBERS.CREATE.commonName), + status: z + .nativeEnum(PkiSubscriberStatus) + .default(PkiSubscriberStatus.ACTIVE) + .describe(PKI_SUBSCRIBERS.CREATE.status), + ttl: z + .string() + .trim() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .describe(PKI_SUBSCRIBERS.CREATE.ttl), + subjectAlternativeNames: validateAltNameField + .array() + .default([]) + .transform((arr) => Array.from(new Set(arr))) + .describe(PKI_SUBSCRIBERS.CREATE.subjectAlternativeNames), + keyUsages: z + .nativeEnum(CertKeyUsage) + .array() + .default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]) + .transform((arr) => Array.from(new Set(arr))) + .describe(PKI_SUBSCRIBERS.CREATE.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .default([]) + .transform((arr) => Array.from(new Set(arr))) + .describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages) + }), + response: { + 200: sanitizedPkiSubscriber + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const subscriber = await server.services.pkiSubscriber.createSubscriber({ + ...req.body, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.CREATE_PKI_SUBSCRIBER, + metadata: { + pkiSubscriberId: subscriber.id, + caId: subscriber.caId ?? undefined, + name: subscriber.name, + commonName: subscriber.commonName, + ttl: subscriber.ttl, + subjectAlternativeNames: subscriber.subjectAlternativeNames, + keyUsages: subscriber.keyUsages as CertKeyUsage[], + extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] + } + } + }); + + return subscriber; + } + }); + + server.route({ + method: "PATCH", + url: "/:subscriberName", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Update PKI Subscriber", + params: z.object({ + subscriberName: z.string().trim().describe(PKI_SUBSCRIBERS.UPDATE.subscriberName) + }), + body: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.UPDATE.projectId), + caId: z + .string() + .trim() + .uuid("CA ID must be a valid UUID") + .min(1, "CA ID is required") + .optional() + .describe(PKI_SUBSCRIBERS.UPDATE.caId), + name: slugSchema({ min: 1, max: 64, field: "name" }).describe(PKI_SUBSCRIBERS.UPDATE.name).optional(), + commonName: z.string().trim().min(1).describe(PKI_SUBSCRIBERS.UPDATE.commonName).optional(), + status: z.nativeEnum(PkiSubscriberStatus).optional().describe(PKI_SUBSCRIBERS.UPDATE.status), + subjectAlternativeNames: validateAltNameField + .array() + .optional() + .describe(PKI_SUBSCRIBERS.UPDATE.subjectAlternativeNames), + ttl: z + .string() + .trim() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .optional() + .describe(PKI_SUBSCRIBERS.UPDATE.ttl), + keyUsages: z + .nativeEnum(CertKeyUsage) + .array() + .transform((arr) => Array.from(new Set(arr))) + .optional() + .describe(PKI_SUBSCRIBERS.UPDATE.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .transform((arr) => Array.from(new Set(arr))) + .optional() + .describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages) + }), + response: { + 200: sanitizedPkiSubscriber + } + }, + handler: async (req) => { + const subscriber = await server.services.pkiSubscriber.updateSubscriber({ + subscriberName: req.params.subscriberName, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.UPDATE_PKI_SUBSCRIBER, + metadata: { + pkiSubscriberId: subscriber.id, + caId: subscriber.caId ?? undefined, + name: subscriber.name, + commonName: subscriber.commonName, + ttl: subscriber.ttl, + subjectAlternativeNames: subscriber.subjectAlternativeNames, + keyUsages: subscriber.keyUsages as CertKeyUsage[], + extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] + } + } + }); + + return subscriber; + } + }); + + server.route({ + method: "DELETE", + url: "/:subscriberName", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Delete PKI Subscriber", + params: z.object({ + subscriberName: z.string().describe(PKI_SUBSCRIBERS.DELETE.subscriberName) + }), + body: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.DELETE.projectId) + }), + response: { + 200: sanitizedPkiSubscriber + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const subscriber = await server.services.pkiSubscriber.deleteSubscriber({ + subscriberName: req.params.subscriberName, + projectId: req.body.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.DELETE_PKI_SUBSCRIBER, + metadata: { + pkiSubscriberId: subscriber.id, + name: subscriber.name + } + } + }); + + return subscriber; + } + }); + + server.route({ + method: "POST", + url: "/:subscriberName/issue-certificate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Issue certificate", + params: z.object({ + subscriberName: z.string().describe(PKI_SUBSCRIBERS.ISSUE_CERT.subscriberName) + }), + body: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.projectId) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificate), + issuingCaCertificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.issuingCaCertificate), + certificateChain: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificateChain), + privateKey: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.privateKey), + serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, subscriber } = + await server.services.pkiSubscriber.issueSubscriberCert({ + subscriberName: req.params.subscriberName, + projectId: req.body.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.ISSUE_PKI_SUBSCRIBER_CERT, + metadata: { + subscriberId: subscriber.id, + name: subscriber.name, + serialNumber + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IssueCert, + distinctId: getTelemetryDistinctId(req), + properties: { + subscriberId: subscriber.id, + commonName: subscriber.commonName, + ...req.auditLogInfo + } + }); + + return { + certificate, + certificateChain, + issuingCaCertificate, + privateKey, + serialNumber + }; + } + }); + + server.route({ + method: "POST", + url: "/:subscriberName/sign-certificate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "Sign certificate", + params: z.object({ + subscriberName: z.string().describe(PKI_SUBSCRIBERS.SIGN_CERT.subscriberName) + }), + body: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.projectId), + csr: z.string().trim().min(1).max(3000).describe(PKI_SUBSCRIBERS.SIGN_CERT.csr) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.certificate), + issuingCaCertificate: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.issuingCaCertificate), + certificateChain: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.certificateChain), + serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, issuingCaCertificate, serialNumber, subscriber } = + await server.services.pkiSubscriber.signSubscriberCert({ + subscriberName: req.params.subscriberName, + projectId: req.body.projectId, + csr: req.body.csr, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: subscriber.projectId, + event: { + type: EventType.SIGN_PKI_SUBSCRIBER_CERT, + metadata: { + subscriberId: subscriber.id, + name: subscriber.name, + serialNumber + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SignCert, + distinctId: getTelemetryDistinctId(req), + properties: { + subscriberId: subscriber.id, + commonName: subscriber.commonName, + ...req.auditLogInfo + } + }); + + return { + certificate, + certificateChain, + issuingCaCertificate, + serialNumber + }; + } + }); + + server.route({ + method: "GET", + url: "/:subscriberName/certificates", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + description: "List PKI Subscriber certificates", + params: z.object({ + subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET.subscriberName) + }), + querystring: z.object({ + projectId: z.string().trim().describe(PKI_SUBSCRIBERS.LIST_CERTS.projectId), + offset: z.coerce.number().min(0).max(100).default(0).describe(PKI_SUBSCRIBERS.LIST_CERTS.offset), + limit: z.coerce.number().min(1).max(100).default(25).describe(PKI_SUBSCRIBERS.LIST_CERTS.limit) + }), + response: { + 200: z.object({ + certificates: z.array(CertificatesSchema), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { totalCount, certificates } = await server.services.pkiSubscriber.listSubscriberCerts({ + subscriberName: req.params.subscriberName, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.LIST_PKI_SUBSCRIBER_CERTS, + metadata: { + subscriberId: req.params.subscriberName, + name: req.params.subscriberName, + projectId: req.query.projectId + } + } + }); + + return { + certificates, + totalCount + }; + } + }); +}; diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 498fb8e8b..3d92bfb1a 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -24,6 +24,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema"; +import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema"; import { ProjectFilterType } from "@app/services/project/project-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; @@ -490,6 +491,38 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/:projectId/pki-subscribers", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSubscribers], + params: z.object({ + projectId: z.string().trim().describe(PROJECTS.LIST_PKI_SUBSCRIBERS.projectId) + }), + response: { + 200: z.object({ + subscribers: z.array(sanitizedPkiSubscriber) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const subscribers = await server.services.project.listProjectPkiSubscribers({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + projectId: req.params.projectId + }); + + return { subscribers }; + } + }); + server.route({ method: "GET", url: "/:projectId/certificate-templates", @@ -628,6 +661,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshHosts], params: z.object({ projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId) }), @@ -666,6 +701,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + hide: false, + tags: [ApiDocsTags.SshHostGroups], params: z.object({ projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOST_GROUPS.projectId) }), diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index e1d7ce5cb..d504e38ed 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1169,7 +1169,7 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.Certificates ); - if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); if (ca.requireTemplateForIssuance && !certificateTemplate) { throw new BadRequestError({ message: "Certificate template is required for issuance" }); @@ -1520,7 +1520,7 @@ export const certificateAuthorityServiceFactory = ({ ); } - if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); if (ca.requireTemplateForIssuance && !certificateTemplate) { throw new BadRequestError({ message: "Certificate template is required for issuance" }); diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index 979a3b9c5..4820cfe00 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -10,6 +10,18 @@ const isValidDate = (dateString: string) => { export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" }); +export const validateAltNameField = z + .string() + .trim() + .refine( + (name) => { + return isFQDN(name) || z.string().email().safeParse(name).success || isValidIp(name); + }, + { + message: "SAN must be a valid hostname, email address, or IP address" + } + ); + export const validateAltNamesField = z .string() .trim() diff --git a/backend/src/services/certificate/certificate-dal.ts b/backend/src/services/certificate/certificate-dal.ts index 71c70838c..aafbe56f4 100644 --- a/backend/src/services/certificate/certificate-dal.ts +++ b/backend/src/services/certificate/certificate-dal.ts @@ -44,8 +44,27 @@ export const certificateDALFactory = (db: TDbClient) => { } }; + const countCertificatesForPkiSubscriber = async (subscriberId: string) => { + try { + interface CountResult { + count: string; + } + + const query = db + .replicaNode()(TableName.Certificate) + .where(`${TableName.Certificate}.pkiSubscriberId`, subscriberId); + + const count = await query.count("*").first(); + + return parseInt((count as unknown as CountResult).count || "0", 10); + } catch (error) { + throw new DatabaseError({ error, name: "Count all subscriber certificates" }); + } + }; + return { ...certificateOrm, - countCertificatesInProject + countCertificatesInProject, + countCertificatesForPkiSubscriber }; }; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-dal.ts b/backend/src/services/pki-subscriber/pki-subscriber-dal.ts new file mode 100644 index 000000000..1899c63a6 --- /dev/null +++ b/backend/src/services/pki-subscriber/pki-subscriber-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TPkiSubscriberDALFactory = ReturnType; + +export const pkiSubscriberDALFactory = (db: TDbClient) => { + const pkiSubscriberOrm = ormify(db, TableName.PkiSubscriber); + return pkiSubscriberOrm; +}; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-schema.ts b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts new file mode 100644 index 000000000..7ffeea3fa --- /dev/null +++ b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts @@ -0,0 +1,14 @@ +import { PkiSubscribersSchema } from "@app/db/schemas"; + +export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({ + id: true, + projectId: true, + caId: true, + name: true, + commonName: true, + status: true, + subjectAlternativeNames: true, + ttl: true, + keyUsages: true, + extendedKeyUsages: true +}); diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts new file mode 100644 index 000000000..5b15786b1 --- /dev/null +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -0,0 +1,805 @@ +/* eslint-disable no-bitwise */ +import { ForbiddenError, subject } from "@casl/ability"; +import * as x509 from "@peculiar/x509"; +import crypto, { KeyObject } from "crypto"; +import { z } from "zod"; + +import { ActionProjectType } from "@app/db/schemas"; +import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { isFQDN } from "@app/lib/validator/validate-url"; +import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; +import { + CertExtendedKeyUsage, + CertExtendedKeyUsageOIDToName, + CertKeyAlgorithm, + CertKeyUsage, + CertStatus +} from "@app/services/certificate/certificate-types"; +import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; +import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { + createSerialNumber, + getCaCertChain, + getCaCredentials, + keyAlgorithmToAlgCfg, + parseDistinguishedName +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; +import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; + +import { + PkiSubscriberStatus, + TCreatePkiSubscriberDTO, + TDeletePkiSubscriberDTO, + TGetPkiSubscriberDTO, + TIssuePkiSubscriberCertDTO, + TListPkiSubscriberCertsDTO, + TSignPkiSubscriberCertDTO, + TUpdatePkiSubscriberDTO +} from "./pki-subscriber-types"; + +type TPkiSubscriberServiceFactoryDep = { + pkiSubscriberDAL: Pick< + TPkiSubscriberDALFactory, + "create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne" + >; + certificateAuthorityDAL: Pick; + certificateAuthorityCertDAL: Pick; + certificateAuthoritySecretDAL: Pick; + certificateAuthorityCrlDAL: Pick; + certificateDAL: Pick; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + projectDAL: Pick; + kmsService: Pick; + permissionService: Pick; +}; + +export type TPkiSubscriberServiceFactory = ReturnType; + +export const pkiSubscriberServiceFactory = ({ + pkiSubscriberDAL, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + certificateAuthoritySecretDAL, + certificateAuthorityCrlDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + projectDAL, + kmsService, + permissionService +}: TPkiSubscriberServiceFactoryDep) => { + const createSubscriber = async ({ + name, + commonName, + status, + caId, + ttl, + subjectAlternativeNames, + keyUsages, + extendedKeyUsages, + projectId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TCreatePkiSubscriberDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.Create, + subject(ProjectPermissionSub.PkiSubscribers, { + name + }) + ); + + const newSubscriber = await pkiSubscriberDAL.create({ + caId, + projectId, + name, + commonName, + status, + ttl, + subjectAlternativeNames, + keyUsages, + extendedKeyUsages + }); + + return newSubscriber; + }; + + const getSubscriber = async ({ + subscriberName, + projectId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetPkiSubscriberDTO) => { + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: subscriber.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.Read, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + return subscriber; + }; + + const updateSubscriber = async ({ + subscriberName, + projectId, + name, + commonName, + status, + caId, + ttl, + subjectAlternativeNames, + keyUsages, + extendedKeyUsages, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdatePkiSubscriberDTO) => { + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: subscriber.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.Edit, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + const updatedSubscriber = await pkiSubscriberDAL.updateById(subscriber.id, { + caId, + name, + commonName, + status, + ttl, + subjectAlternativeNames, + keyUsages, + extendedKeyUsages + }); + + return updatedSubscriber; + }; + + const deleteSubscriber = async ({ + subscriberName, + projectId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TDeletePkiSubscriberDTO) => { + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: subscriber.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.Delete, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + await pkiSubscriberDAL.deleteById(subscriber.id); + + return subscriber; + }; + + const issueSubscriberCert = async ({ + subscriberName, + projectId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TIssuePkiSubscriberCertDTO) => { + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" }); + + const ca = await certificateAuthorityDAL.findById(subscriber.caId); + if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.IssueCert, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + if (subscriber.status !== PkiSubscriberStatus.ACTIVE) + throw new BadRequestError({ message: "Subscriber is not active" }); + if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + const decryptedCaCert = await kmsDecryptor({ + cipherTextBlob: caCert.encryptedCertificate + }); + + const caCertObj = new x509.X509Certificate(decryptedCaCert); + const notBeforeDate = new Date(); + const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl)); + const caCertNotBeforeDate = new Date(caCertObj.notBefore); + const caCertNotAfterDate = new Date(caCertObj.notAfter); + + // check not before constraint + if (notBeforeDate < caCertNotBeforeDate) { + throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); + } + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ + name: `CN=${subscriber.commonName}`, + keys: leafKeys, + signingAlgorithm: alg, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment) + ], + attributes: [new x509.ChallengePasswordAttribute("password")] + }); + + const { caPrivateKey, caSecret } = await getCaCredentials({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + + const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); + const appCfg = getConfig(); + + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + new x509.CRLDistributionPointsExtension([distributionPointUrl]), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), + new x509.AuthorityInfoAccessExtension({ + caIssuers: new x509.GeneralName("url", caIssuerUrl) + }), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy + ]; + + const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[]; + const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0); + if (keyUsagesBitValue) { + extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true)); + } + + if (subscriber.extendedKeyUsages.length) { + const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension( + subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]), + true + ); + extensions.push(extendedKeyUsagesExtension); + } + + let altNamesArray: { type: "email" | "dns"; value: string }[] = []; + + if (subscriber.subjectAlternativeNames?.length) { + altNamesArray = subscriber.subjectAlternativeNames.map((altName) => { + if (z.string().email().safeParse(altName).success) { + return { type: "email", value: altName }; + } + + if (isFQDN(altName, { allow_wildcard: true })) { + return { type: "dns", value: altName }; + } + + throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); + }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); + } + + const serialNumber = createSerialNumber(); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: csrObj.subject, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: csrObj.publicKey, + signingAlgorithm: alg, + extensions + }); + + const skLeafObj = KeyObject.from(leafKeys.privateKey); + const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(leafCert.rawData)) + }); + const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ + plainText: Buffer.from(skLeaf) + }); + + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caCertId: caCert.id, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim(); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); + + await certificateDAL.transaction(async (tx) => { + const cert = await certificateDAL.create( + { + caId: ca.id, + caCertId: caCert.id, + pkiSubscriberId: subscriber.id, + status: CertStatus.ACTIVE, + friendlyName: subscriber.commonName, + commonName: subscriber.commonName, + altNames: subscriber.subjectAlternativeNames.join(","), + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate, + keyUsages: selectedKeyUsages, + extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] + }, + tx + ); + + await certificateBodyDAL.create( + { + certId: cert.id, + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + await certificateSecretDAL.create( + { + certId: cert.id, + encryptedPrivateKey + }, + tx + ); + }); + + return { + certificate: leafCert.toString("pem"), + certificateChain: certificateChainPem, + issuingCaCertificate, + privateKey: skLeaf, + serialNumber, + ca, + subscriber + }; + }; + + const signSubscriberCert = async ({ + subscriberName, + projectId, + csr, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TSignPkiSubscriberCertDTO) => { + const appCfg = getConfig(); + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" }); + + const ca = await certificateAuthorityDAL.findById(subscriber.caId); + if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.IssueCert, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + if (subscriber.status !== PkiSubscriberStatus.ACTIVE) + throw new BadRequestError({ message: "Subscriber is not active" }); + if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } + const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + const kmsDecryptor = await kmsService.decryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + const decryptedCaCert = await kmsDecryptor({ + cipherTextBlob: caCert.encryptedCertificate + }); + + const caCertObj = new x509.X509Certificate(decryptedCaCert); + const notBeforeDate = new Date(); + const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl)); + const caCertNotBeforeDate = new Date(caCertObj.notBefore); + const caCertNotAfterDate = new Date(caCertObj.notAfter); + + // check not before constraint + if (notBeforeDate < caCertNotBeforeDate) { + throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" }); + } + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm); + + const csrObj = new x509.Pkcs10CertificateRequest(csr); + + const dn = parseDistinguishedName(csrObj.subject); + const cn = dn.commonName; + if (cn !== subscriber.commonName) { + throw new BadRequestError({ message: "Common name (CN) in the CSR does not match the subscriber's common name" }); + } + + const { caPrivateKey, caSecret } = await getCaCredentials({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + + const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), + new x509.CRLDistributionPointsExtension([distributionPointUrl]), + new x509.AuthorityInfoAccessExtension({ + caIssuers: new x509.GeneralName("url", caIssuerUrl) + }), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy + ]; + + // handle key usages + const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension; + let csrKeyUsages: CertKeyUsage[] = []; + if (csrKeyUsageExtension) { + csrKeyUsages = Object.values(CertKeyUsage).filter( + (keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0 + ); + } + + const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[]; + + if (csrKeyUsages.some((keyUsage) => !selectedKeyUsages.includes(keyUsage))) { + throw new BadRequestError({ + message: "Invalid key usage value based on subscriber's specified key usages" + }); + } + + const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0); + if (keyUsagesBitValue) { + extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true)); + } + + // handle extended key usages + const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension; + let csrExtendedKeyUsages: CertExtendedKeyUsage[] = []; + if (csrExtendedKeyUsageExtension) { + csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map( + (ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string] + ); + } + + const selectedExtendedKeyUsages = subscriber.extendedKeyUsages as CertExtendedKeyUsage[]; + if (csrExtendedKeyUsages.some((eku) => !selectedExtendedKeyUsages.includes(eku))) { + throw new BadRequestError({ + message: "Invalid extended key usage value based on subscriber's specified extended key usages" + }); + } + + if (selectedExtendedKeyUsages.length) { + extensions.push( + new x509.ExtendedKeyUsageExtension( + selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]), + true + ) + ); + } + + // attempt to read from CSR if altNames is not explicitly provided + let altNamesArray: { + type: "email" | "dns"; + value: string; + }[] = []; + + const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17"); + if (sanExtension) { + const sanNames = new x509.GeneralNames(sanExtension.value); + + altNamesArray = sanNames.items + .filter((value) => value.type === "email" || value.type === "dns") + .map((name) => ({ + type: name.type as "email" | "dns", + value: name.value + })); + } + + if ( + altNamesArray + .map((altName) => altName.value) + .some((altName) => !subscriber.subjectAlternativeNames.includes(altName)) + ) { + throw new BadRequestError({ + message: "Invalid subject alternative name based on subscriber's specified subject alternative names" + }); + } + + if (altNamesArray.length) { + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); + } + + const serialNumber = createSerialNumber(); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: csrObj.subject, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: csrObj.publicKey, + signingAlgorithm: alg, + extensions + }); + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(leafCert.rawData)) + }); + + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caCertId: ca.activeCaCertId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim(); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); + + await certificateDAL.transaction(async (tx) => { + const cert = await certificateDAL.create( + { + caId: ca.id, + caCertId: caCert.id, + pkiSubscriberId: subscriber.id, + status: CertStatus.ACTIVE, + friendlyName: subscriber.commonName, + commonName: subscriber.commonName, + altNames: subscriber.subjectAlternativeNames.join(","), + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate, + keyUsages: selectedKeyUsages, + extendedKeyUsages: selectedExtendedKeyUsages + }, + tx + ); + + await certificateBodyDAL.create( + { + certId: cert.id, + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + return cert; + }); + + return { + certificate: leafCert.toString("pem"), + certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), + issuingCaCertificate, + serialNumber, + ca, + commonName: subscriber.commonName, + subscriber + }; + }; + + const listSubscriberCerts = async ({ + subscriberName, + projectId, + offset, + limit, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TListPkiSubscriberCertsDTO) => { + const subscriber = await pkiSubscriberDAL.findOne({ + name: subscriberName, + projectId + }); + if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` }); + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: subscriber.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiSubscriberActions.ListCerts, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + + const certificates = await certificateDAL.find( + { + pkiSubscriberId: subscriber.id + }, + { offset, limit, sort: [["updatedAt", "desc"]] } + ); + + const count = await certificateDAL.countCertificatesForPkiSubscriber(subscriber.id); + + return { + certificates, + totalCount: count + }; + }; + + return { + createSubscriber, + getSubscriber, + updateSubscriber, + deleteSubscriber, + issueSubscriberCert, + signSubscriberCert, + listSubscriberCerts + }; +}; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-types.ts b/backend/src/services/pki-subscriber/pki-subscriber-types.ts new file mode 100644 index 000000000..690148f16 --- /dev/null +++ b/backend/src/services/pki-subscriber/pki-subscriber-types.ts @@ -0,0 +1,54 @@ +import { TProjectPermission } from "@app/lib/types"; + +import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types"; + +export enum PkiSubscriberStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export type TCreatePkiSubscriberDTO = { + caId: string; + name: string; + commonName: string; + status: PkiSubscriberStatus; + ttl: string; + subjectAlternativeNames: string[]; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; +} & TProjectPermission; + +export type TGetPkiSubscriberDTO = { + subscriberName: string; +} & TProjectPermission; + +export type TUpdatePkiSubscriberDTO = { + subscriberName: string; + caId?: string; + name?: string; + commonName?: string; + status?: PkiSubscriberStatus; + ttl?: string; + subjectAlternativeNames?: string[]; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; +} & TProjectPermission; + +export type TDeletePkiSubscriberDTO = { + subscriberName: string; +} & TProjectPermission; + +export type TIssuePkiSubscriberCertDTO = { + subscriberName: string; +} & TProjectPermission; + +export type TSignPkiSubscriberCertDTO = { + subscriberName: string; + csr: string; +} & TProjectPermission; + +export type TListPkiSubscriberCertsDTO = { + subscriberName: string; + offset: number; + limit: number; +} & TProjectPermission; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 7ab45baa7..8cfa20697 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionCertificateActions, + ProjectPermissionPkiSubscriberActions, ProjectPermissionSecretActions, ProjectPermissionSshHostActions, ProjectPermissionSub @@ -35,6 +36,7 @@ import { groupBy } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectPermission } from "@app/lib/types"; import { TQueueServiceFactory } from "@app/queue"; +import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { ActorType } from "../auth/auth-type"; import { TCertificateDALFactory } from "../certificate/certificate-dal"; @@ -86,6 +88,7 @@ import { TListProjectCasDTO, TListProjectCertificateTemplatesDTO, TListProjectCertsDTO, + TListProjectPkiSubscribersDTO, TListProjectsDTO, TListProjectSshCasDTO, TListProjectSshCertificatesDTO, @@ -145,6 +148,7 @@ type TProjectServiceFactoryDep = { "findById" | "findByIdWithWorkflowIntegrationDetails" >; projectUserMembershipRoleDAL: Pick; + pkiSubscriberDAL: Pick; certificateAuthorityDAL: Pick; certificateDAL: Pick; certificateTemplateDAL: Pick; @@ -207,6 +211,7 @@ export const projectServiceFactory = ({ certificateTemplateDAL, pkiCollectionDAL, pkiAlertDAL, + pkiSubscriberDAL, sshCertificateAuthorityDAL, sshCertificateAuthoritySecretDAL, sshCertificateDAL, @@ -1057,6 +1062,45 @@ export const projectServiceFactory = ({ }; }; + /** + * Return list of PKI subscribers for project + */ + const listProjectPkiSubscribers = async ({ + actorId, + actorOrgId, + actorAuthMethod, + actor, + projectId + }: TListProjectPkiSubscribersDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + const allowedSubscribers = []; + + // (dangtony98): room to optimize + const subscribers = await pkiSubscriberDAL.find({ projectId }); + + for (const subscriber of subscribers) { + const canRead = permission.can( + ProjectPermissionPkiSubscriberActions.Read, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriber.name + }) + ); + if (canRead) { + allowedSubscribers.push(subscriber); + } + } + + return allowedSubscribers; + }; + /** * Return list of certificate templates for project */ @@ -1156,17 +1200,15 @@ export const projectServiceFactory = ({ const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId); for (const host of hosts) { - try { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionSshHostActions.Read, - subject(ProjectPermissionSub.SshHosts, { - hostname: host.hostname - }) - ); + const canRead = permission.can( + ProjectPermissionSshHostActions.Read, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); + if (canRead) { allowedHosts.push(host); - } catch { - // intentionally ignore projects where user lacks access } } @@ -1930,6 +1972,7 @@ export const projectServiceFactory = ({ listProjectSshCas, listProjectSshHosts, listProjectSshHostGroups, + listProjectPkiSubscribers, listProjectSshCertificates, listProjectSshCertificateTemplates, updateVersionLimit, diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index dc26d2357..9f74e123c 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -155,6 +155,7 @@ export type TListProjectCertificateTemplatesDTO = TProjectPermission; export type TListProjectSshCasDTO = TProjectPermission; export type TListProjectSshHostsDTO = TProjectPermission; export type TListProjectSshCertificateTemplatesDTO = TProjectPermission; +export type TListProjectPkiSubscribersDTO = TProjectPermission; export type TListProjectSshCertificatesDTO = { offset: number; limit: number; diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index 9e046cdbd..a370d0332 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -189,6 +189,7 @@ export type TSignCertificateEvent = { properties: { caId?: string; certificateTemplateId?: string; + subscriberId?: string; commonName: string; userAgent?: string; }; @@ -199,6 +200,7 @@ export type TIssueCertificateEvent = { properties: { caId?: string; certificateTemplateId?: string; + subscriberId?: string; commonName: string; userAgent?: string; }; diff --git a/docs/api-reference/endpoints/pki/subscribers/create.mdx b/docs/api-reference/endpoints/pki/subscribers/create.mdx new file mode 100644 index 000000000..14a53b7fa --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/pki/subscribers" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/delete.mdx b/docs/api-reference/endpoints/pki/subscribers/delete.mdx new file mode 100644 index 000000000..5975b89e9 --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/pki/subscribers/{subscriberName}" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/issue-cert.mdx b/docs/api-reference/endpoints/pki/subscribers/issue-cert.mdx new file mode 100644 index 000000000..be57ab01b --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/issue-cert.mdx @@ -0,0 +1,4 @@ +--- +title: "Issue Certificate" +openapi: "POST /api/v1/pki/subscribers/{subscriberName}/issue-cert" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/list-certs.mdx b/docs/api-reference/endpoints/pki/subscribers/list-certs.mdx new file mode 100644 index 000000000..3a4607303 --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/list-certs.mdx @@ -0,0 +1,4 @@ +--- +title: "List Certificates" +openapi: "GET /api/v1/pki/subscribers/{subscriberName}/certificates" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/read.mdx b/docs/api-reference/endpoints/pki/subscribers/read.mdx new file mode 100644 index 000000000..0d223217d --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/read.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/pki/subscribers/{subscriberName}" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/sign-cert.mdx b/docs/api-reference/endpoints/pki/subscribers/sign-cert.mdx new file mode 100644 index 000000000..d31d30239 --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/sign-cert.mdx @@ -0,0 +1,4 @@ +--- +title: "Sign Certificate" +openapi: "POST /api/v1/pki/subscribers/{subscriberName}/sign-certificate" +--- diff --git a/docs/api-reference/endpoints/pki/subscribers/update.mdx b/docs/api-reference/endpoints/pki/subscribers/update.mdx new file mode 100644 index 000000000..5b62cbe7d --- /dev/null +++ b/docs/api-reference/endpoints/pki/subscribers/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/pki/subscribers/{subscriberName}" +--- diff --git a/docs/api-reference/endpoints/ssh/groups/add-host.mdx b/docs/api-reference/endpoints/ssh/groups/add-host.mdx index 9f903eccd..77257cd40 100644 --- a/docs/api-reference/endpoints/ssh/groups/add-host.mdx +++ b/docs/api-reference/endpoints/ssh/groups/add-host.mdx @@ -1,4 +1,4 @@ --- title: "Add Host" -openapi: "POST /api/v1/ssh/host-groups/{sshHostGroupId}/hosts" +openapi: "POST /api/v1/ssh/host-groups/{sshHostGroupId}/hosts/{hostId}" --- diff --git a/docs/api-reference/endpoints/ssh/groups/remove-host.mdx b/docs/api-reference/endpoints/ssh/groups/remove-host.mdx index 6933e5c9f..b1de7f4ae 100644 --- a/docs/api-reference/endpoints/ssh/groups/remove-host.mdx +++ b/docs/api-reference/endpoints/ssh/groups/remove-host.mdx @@ -1,4 +1,4 @@ --- title: "Remove Host" -openapi: "DELETE /api/v1/ssh/host-groups/{sshHostGroupId}/hosts/{sshHostId}" +openapi: "DELETE /api/v1/ssh/host-groups/{sshHostGroupId}/hosts/{hostId}" --- diff --git a/docs/api-reference/endpoints/ssh/hosts/list-my.mdx b/docs/api-reference/endpoints/ssh/hosts/list-my.mdx index 2b7ab51c0..6ccc4e325 100644 --- a/docs/api-reference/endpoints/ssh/hosts/list-my.mdx +++ b/docs/api-reference/endpoints/ssh/hosts/list-my.mdx @@ -1,4 +1,4 @@ --- title: "List My Hosts" -openapi: "GET /api/v1/ssh/hosts/" +openapi: "GET /api/v1/ssh/hosts" --- diff --git a/docs/documentation/platform/github-org-sync.mdx b/docs/documentation/platform/github-org-sync.mdx index 00c9bf4c4..519e12db8 100644 --- a/docs/documentation/platform/github-org-sync.mdx +++ b/docs/documentation/platform/github-org-sync.mdx @@ -13,7 +13,7 @@ To enable and configure GitHub Organization Synchronization, follow these steps: - 1. Navigate to **Organization Settings** and select the **Security Tab**. + 1. Navigate to the **Single Sign-On (SSO)** page and select the **Provisioning** tab. ![config](../../images/platform/external-syncs/github-org-sync-section.png) 2. Click the **Configure** button and provide the name of your GitHub Organization. ![config-modal](../../images/platform/external-syncs/github-org-sync-config-modal.png) diff --git a/docs/documentation/platform/ldap/general.mdx b/docs/documentation/platform/ldap/general.mdx index 939eaa727..c1d062ef5 100644 --- a/docs/documentation/platform/ldap/general.mdx +++ b/docs/documentation/platform/ldap/general.mdx @@ -18,7 +18,9 @@ Prerequisites: - In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Select **Connect** for **LDAP**. + + ![LDAP SSO Connect](../../../images/sso/connect-ldap.png) Next, input your LDAP server settings. diff --git a/docs/documentation/platform/ldap/jumpcloud.mdx b/docs/documentation/platform/ldap/jumpcloud.mdx index 39579b785..d520598d1 100644 --- a/docs/documentation/platform/ldap/jumpcloud.mdx +++ b/docs/documentation/platform/ldap/jumpcloud.mdx @@ -27,7 +27,9 @@ Prerequisites: ![LDAP JumpCloud](/images/platform/ldap/jumpcloud/ldap-jumpcloud-enable-bind-dn.png) - In Infisical, head to your Organization Settings > Security > LDAP and select **Manage**. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Select **Connect** for **LDAP**. + + ![LDAP SSO Connect](../../../images/sso/connect-ldap.png) Next, input your JumpCloud LDAP server settings. diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index 4976b5e18..f1c434e9c 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -75,8 +75,8 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. Here's some guidance on each field: - Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty. - - Common Name (CN): The (common) name for the certificate like `service.acme.com`. - - Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`. + - Common Name (CN): The common name for the certificate like `service.acme.com`. + - Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be hostnames or email addresses like `app1.acme.com, app2.acme.com`. - TTL: The lifetime of the certificate in seconds. - Key Usage: The key usage extension of the certificate. - Extended Key Usage: The extended key usage extension of the certificate. @@ -240,7 +240,7 @@ openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem ``` Note that you can also obtain the CRL from the certificate itself by -referencing the CRL distribution point extension on the certificate itself. +referencing the CRL distribution point extension on the certificate. To check a certificate against the CRL distribution point specified within it with OpenSSL, you can use the following command: diff --git a/docs/documentation/platform/pki/overview.mdx b/docs/documentation/platform/pki/overview.mdx index 259f15a5d..8ee9b113d 100644 --- a/docs/documentation/platform/pki/overview.mdx +++ b/docs/documentation/platform/pki/overview.mdx @@ -4,9 +4,10 @@ sidebarTitle: "Overview" description: "Learn how to create a Private CA hierarchy and issue X.509 certificates." --- -Infisical can be used to create a Private Certificate Authority (CA) hierarchy and issue X.509 certificates for internal use. This allows you to manage your own PKI infrastructure and issue digital certificates for services, applications, and devices. +Infisical can be used to create a Private Certificate Authority (CA) hierarchy and issue X.509 certificates for internal use. This allows you to manage your own PKI infrastructure and issue digital certificates for subscribers such as services, applications, and devices. -Infisical's internal PKI offering is split into two modules: +Infisical's PKI offering is split into three components: -- [Private CA](/documentation/platform/pki/private-ca): Infisical lets you create private CAs, including root and intermediary CAs. -- [Certificates](/documentation/platform/pki/certificates): Infisical allows you to issue X.509 certificates using the private CAs you create. +- [Certificate Authorities](/documentation/platform/pki/private-ca): Create and manage private CAs, including root and intermediate CAs. +- [Subscribers](/documentation/platform/pki/subscribers): Define and manage entities that will request X.509 certificates from CAs. This module provides a centralized view of all subscribers, enabling you to issue certificates and monitor their status. +- [Certificates](/documentation/platform/pki/certificates): Track and monitor issued X.509 certificates, maintaining a comprehensive inventory of all active and expired certificates. diff --git a/docs/documentation/platform/pki/private-ca.mdx b/docs/documentation/platform/pki/private-ca.mdx index d7f3f896c..7d7ee1220 100644 --- a/docs/documentation/platform/pki/private-ca.mdx +++ b/docs/documentation/platform/pki/private-ca.mdx @@ -7,7 +7,7 @@ description: "Learn how to create a Private CA hierarchy with Infisical." ## Concept The first step to creating your Internal PKI is to create a Private Certificate Authority (CA) hierarchy that is a structure of entities -used to issue digital certificates for services, applications, and devices. +used to issue digital certificates for your [subscribers](/documentation/platform/pki/subscribers).
@@ -24,7 +24,7 @@ graph TD A typical workflow for setting up a Private CA hierarchy consists of the following steps: -1. Configuring an Infisical root CA with details like name, validity period, and path length — This step is optional if you wish to use an external root CA. +1. Configuring an Infisical root CA with details like name, validity period, and path length — This step is optional if you wish to use an external root CA with Infisical only serving the intermediate CAs. 2. Configuring and chaining intermediate CA(s) with details like name, validity period, path length, and imported certificate to your Root CA. 3. Managing the CA lifecycle events such as CA succession. @@ -99,7 +99,7 @@ consisting of an (optional) root CA and an intermediate CA. ![pki cas](/images/platform/pki/ca/cas.png) Great! You've successfully created a Private CA hierarchy with a root CA and an intermediate CA. - Now check out the [Certificates](/documentation/platform/pki/certificates) page to learn more about how to issue X.509 certificates using the intermediate CA. + Now check out the [Subscribers](/documentation/platform/pki/subscribers) page to learn more about how to issue X.509 certificates using the intermediate CA. 2.3b. If you have an external root CA, select **External CA** for the **Parent CA Type** field. @@ -110,7 +110,7 @@ consisting of an (optional) root CA and an intermediate CA. Finally, press **Install** to import the certificate and certificate chain as part of the installation step for the intermediate CA Great! You've successfully created a Private CA hierarchy with an intermediate CA chained to an external root CA. - Now check out the [Certificates](/documentation/platform/pki/certificates) page to learn more about how to issue X.509 certificates using the intermediate CA. + Now check out the [Subscribers](/documentation/platform/pki/subscribers) page to learn more about how to issue X.509 certificates using the intermediate CA. @@ -255,7 +255,7 @@ consisting of an (optional) root CA and an intermediate CA. } ``` - Great! You’ve successfully created a Private CA hierarchy with a root CA and an intermediate CA. Now check out the Certificates page to learn more about how to issue X.509 certificates using the intermediate CA. + Great! You’ve successfully created a Private CA hierarchy with a root CA and an intermediate CA. Now check out the [Subscribers](/documentation/platform/pki/subscribers) page to learn more about how to issue X.509 certificates using the intermediate CA. diff --git a/docs/documentation/platform/pki/subscribers.mdx b/docs/documentation/platform/pki/subscribers.mdx new file mode 100644 index 000000000..3aebe50e2 --- /dev/null +++ b/docs/documentation/platform/pki/subscribers.mdx @@ -0,0 +1,130 @@ +--- +title: "Subscribers" +sidebarTitle: "Subscribers" +description: "Learn how to manage PKI subscribers and issue X.509 certificates for them." +--- + +## Concept + +In Infisical PKI, subscribers are logical representations of entities such as devices, servers, applications that request and receive certificates from Certificate Authorities (CAs). + +
+ +```mermaid +graph TD +A[Issuing CA] --> C1[Certificate] + C1 --> S1[Subscriber] + A --> C2[Certificate] + C2 --> S2[Subscriber] +``` + +
+ +## Workflow + +The typical workflow for managing subscribers consists of the following steps: + +1. Creating a subscriber and defining which (issuing) CA will issue X.509 certificates for it as well as attributes to be included on the certificates including common name, subject alternative names, TLL, etc. +2. Requesting for a certificate against the subscriber with or without a certificate signing request (CSR). +3. Managing certificate lifecycle events such as certificate renewal and revocation. As part of the certificate revocation flow, + you can also query for a Certificate Revocation List [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list), a time-stamped, signed + data structure issued by a CA containing a list of revoked certificates to check if a certificate has been revoked. + + + Note that this workflow can be executed via the Infisical UI or manually such + as via API. + + +## Guide to Issuing Certificates with Subscribers + +In the following steps, we explore how to issue a X.509 certificate for a subscriber. + + + + A subscriber is the logical representation of an entity that requests and + receives certificates from a CA. With a subscriber, you can specify the + attributes that must be present on the X.509 certificates issued for it. + + Head to your Infisical PKI Project > Subscribers to create a subscriber. + + ![pki create subscriber](/images/platform/pki/subscriber/subscriber-create.png) + + ![pki create subscriber 2](/images/platform/pki/subscriber/subscriber-create-2.png) + + Here's some guidance on each field. + + - Subscriber Name: A slug-friendly name for the subscriber such as `web-service`. + - Issuing CA: The Certificate Authority (CA) that will issue X.509 certificates for the subscriber. + - Common Name (CN): The common name to be included on certificates to be issued to the subscriber. + - Subject Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) to be included on certificates; these can be hostnames or email addresses like `app1.acme.com, app2.acme.com`. + - TTL: The lifetime of the certificate. + - Key Usage: The key usage extension of the certificate. + - Extended Key Usage: The extended key usage extension of the certificate. + + + It's possible to issue certificates for a subscriber with or without a certificate signing request (CSR). + - If requesting without a CSR, the attributes specified on the subscriber will be used to issue a certificate for the subscriber. + - If requesting with a CSR, the attributes on it will be validated against the attributes specified on the subscriber + and a certificate is only issued if they comply. + + + + + Once you have created a subscriber from step 1, you can issue a certificate for it. + + Press on the subscriber you want to issue a certificate for and click on the **Issue Certificate** button on that subscriber's page. + + ![pki issue subscriber certificate](/images/platform/pki/subscriber/subscriber-issue-cert.png) + + ![pki issue subscriber certificate 2](/images/platform/pki/subscriber/subscriber-issue-cert-2.png) + + + + +## Guide to Revoking Certificates + +In the following steps, we explore how to revoke a X.509 certificate and obtain a Certificate Revocation List (CRL) for a CA. + + + + Assuming that you've issued a certificate for a subscriber, you can revoke it by + selecting the **Revoke Certificate** option on the certificate you wish to revoke + on the subscriber's page. + + ![pki revoke subscriber certificate](/images/platform/pki/subscriber/subscriber-revoke-cert.png) + + + + In order to check the revocation status of a certificate, you can check it + against the CRL of a CA by heading to its Issuing CA and downloading the CRL. + + ![pki view crl](/images/platform/pki/subscriber/subscriber-ca-crl.png) + + To verify a certificate against the + downloaded CRL with OpenSSL, you can use the following command: + +```bash +openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem +``` + +Note that you can also obtain the CRL from the certificate itself by +referencing the CRL distribution point extension on the certificate. + +To check a certificate against the CRL distribution point specified within it with OpenSSL, you can use the following command: + +```bash +openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem +``` + + + + +## FAQ + + + + To renew a certificate, you have to issue a new certificate for the same + subscriber. The original certificate will continue to be valid through its + original TTL unless explicitly revoked. + + diff --git a/docs/documentation/platform/scim/azure.mdx b/docs/documentation/platform/scim/azure.mdx index 0e86f6149..e755f8750 100644 --- a/docs/documentation/platform/scim/azure.mdx +++ b/docs/documentation/platform/scim/azure.mdx @@ -15,7 +15,7 @@ Prerequisites: - In Infisical, head to your Organization Settings > Security > SCIM Configuration and + In Infisical, head to the **Single Sign-On (SSO)** page and select the **Provisioning** tab. Under SCIM Configuration, press the **Enable SCIM provisioning** toggle to allow Azure to provision/deprovision users for your organization. ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) diff --git a/docs/documentation/platform/scim/jumpcloud.mdx b/docs/documentation/platform/scim/jumpcloud.mdx index 42d33247a..be4caf738 100644 --- a/docs/documentation/platform/scim/jumpcloud.mdx +++ b/docs/documentation/platform/scim/jumpcloud.mdx @@ -15,7 +15,7 @@ Prerequisites: - In Infisical, head to your Organization Settings > Security > SCIM Configuration and + In Infisical, head to the **Single Sign-On (SSO)** page and select the **Provisioning** tab. Under SCIM Configuration, press the **Enable SCIM provisioning** toggle to allow JumpCloud to provision/deprovision users and user groups for your organization. ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) diff --git a/docs/documentation/platform/scim/okta.mdx b/docs/documentation/platform/scim/okta.mdx index d33bd242d..cf2c17724 100644 --- a/docs/documentation/platform/scim/okta.mdx +++ b/docs/documentation/platform/scim/okta.mdx @@ -15,7 +15,7 @@ Prerequisites: - In Infisical, head to your Organization Settings > Security > SCIM Configuration and + In Infisical, head to the **Single Sign-On (SSO)** page and select the **Provisioning** tab. Under SCIM Configuration, press the **Enable SCIM provisioning** toggle to allow Okta to provision/deprovision users and user groups for your organization. ![SCIM enable provisioning](/images/platform/scim/scim-enable-provisioning.png) diff --git a/docs/documentation/platform/sso/auth0-oidc.mdx b/docs/documentation/platform/sso/auth0-oidc.mdx index e8b532c1c..0665a7b30 100644 --- a/docs/documentation/platform/sso/auth0-oidc.mdx +++ b/docs/documentation/platform/sso/auth0-oidc.mdx @@ -39,8 +39,8 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." - 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click **Connect**. - ![OIDC auth0 manage org Infisical](../../../images/sso/auth0-oidc/org-oidc-overview.png) + 3.1. Back in Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **OIDC**. + ![OIDC SSO Connect](../../../images/sso/connect-oidc.png) 3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret** from step 2.1 and 2.2. ![OIDC auth0 paste values into Infisical](../../../images/sso/auth0-oidc/org-update-oidc.png) diff --git a/docs/documentation/platform/sso/auth0-saml.mdx b/docs/documentation/platform/sso/auth0-saml.mdx index b426d1aae..562360ecb 100644 --- a/docs/documentation/platform/sso/auth0-saml.mdx +++ b/docs/documentation/platform/sso/auth0-saml.mdx @@ -12,7 +12,9 @@ description: "Learn how to configure Auth0 SAML for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Auth0, then click **Connect** again. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Auth0**, then click **Connect** again. + + ![SSO connect section](../../../images/sso/connect-saml.png) Next, note the **Application Callback URL** and **Audience** to use when configuring the Auth0 SAML application. diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index 282cddae5..137dc6564 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -12,7 +12,9 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Azure / Entra, then click **Connect** again. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Azure / Entra**, then click **Connect** again. + + ![SSO connect section](../../../images/sso/connect-saml.png) Next, copy the **Reply URL (Assertion Consumer Service URL)** and **Identifier (Entity ID)** to use when configuring the Azure SAML application. diff --git a/docs/documentation/platform/sso/general-oidc.mdx b/docs/documentation/platform/sso/general-oidc.mdx index 76e364b2f..a10b05cfc 100644 --- a/docs/documentation/platform/sso/general-oidc.mdx +++ b/docs/documentation/platform/sso/general-oidc.mdx @@ -28,8 +28,8 @@ Prerequisites: 1.4. Access the IdP’s OIDC discovery document (usually located at `https:///.well-known/openid-configuration`). This document contains important endpoints such as authorization, token, userinfo, and keys. - 2.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. - ![OIDC general manage org Infisical](../../../images/sso/general-oidc/org-oidc-manage.png) + 2.1. Back in Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Select **Connect** for **OIDC**. + ![OIDC SSO Connect](../../../images/sso/connect-oidc.png) 2.2. You can configure OIDC either through the Discovery URL (Recommended) or by inputting custom endpoints. diff --git a/docs/documentation/platform/sso/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx index 87ffa8412..99223c815 100644 --- a/docs/documentation/platform/sso/google-saml.mdx +++ b/docs/documentation/platform/sso/google-saml.mdx @@ -12,7 +12,9 @@ description: "Learn how to configure Google SAML for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Google, then click **Connect** again. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Google**, then click **Connect** again. + + ![SSO connect section](../../../images/sso/connect-saml.png) Next, note the **ACS URL** and **SP Entity ID** to use when configuring the Google SAML application. diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx index 6ca20c752..0898c0715 100644 --- a/docs/documentation/platform/sso/jumpcloud.mdx +++ b/docs/documentation/platform/sso/jumpcloud.mdx @@ -12,7 +12,9 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select JumpCloud, then click **Connect** again. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **JumpCloud**, then click **Connect** again. + + ![SSO connect section](../../../images/sso/connect-saml.png) Next, copy the **ACS URL** and **SP Entity ID** to use when configuring the JumpCloud SAML application. diff --git a/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx b/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx index c423bac5a..29bca5a8d 100644 --- a/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx +++ b/docs/documentation/platform/sso/keycloak-oidc/group-membership-mapping.mdx @@ -53,7 +53,7 @@ Infisical groups not present in their groups claim. 2.1. In Infisical, create any groups you would like to sync users to. Make sure the name of the Infisical group is an exact match of the Keycloak group name. ![OIDC keycloak infisical group](/images/sso/keycloak-oidc/group-membership-mapping/create-infisical-group.png) - 2.2. Next, enable **OIDC Group Membership Mapping** in Organization Settings > Security. + 2.2. Next, enable **OIDC Group Membership Mapping** on the **Single Sign-On (SSO)** page under the **General** tab. ![OIDC keycloak enable group membership mapping](/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png) 2.3. The next time a user logs in they will be synced to their matching Keycloak groups. diff --git a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx index 803818a0e..06d8dfa43 100644 --- a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx +++ b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx @@ -66,8 +66,8 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO." - 3.1. Back in Infisical, in the Organization settings > Security > OIDC, click Connect. - ![OIDC keycloak manage org Infisical](/images/sso/keycloak-oidc/manage-org-oidc.png) + 3.1. Back in Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **OIDC**. + ![OIDC SSO Connect](../../../../images/sso/connect-oidc.png) 3.2. For configuration type, select Discovery URL. Then, set the appropriate values for **Discovery Document URL**, **JWT Signature Algorithm**, **Client ID**, and **Client Secret**. ![OIDC keycloak paste values into Infisical](/images/sso/keycloak-oidc/create-oidc.png) diff --git a/docs/documentation/platform/sso/keycloak-saml.mdx b/docs/documentation/platform/sso/keycloak-saml.mdx index 7e4004122..ba6aa0c3a 100644 --- a/docs/documentation/platform/sso/keycloak-saml.mdx +++ b/docs/documentation/platform/sso/keycloak-saml.mdx @@ -12,9 +12,9 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Keycloak, then click **Connect** again. + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Keycloak**, then click **Connect** again. - ![Keycloak SAML organization security section](../../../images/sso/keycloak/org-security-section.png) + ![SSO connect section](../../../images/sso/connect-saml.png) Next, copy the **Valid redirect URI** and **SP Entity ID** to use when configuring the Keycloak SAML application. diff --git a/docs/documentation/platform/sso/okta.mdx b/docs/documentation/platform/sso/okta.mdx index 1abd03d6f..2af689e4c 100644 --- a/docs/documentation/platform/sso/okta.mdx +++ b/docs/documentation/platform/sso/okta.mdx @@ -12,8 +12,10 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO." - In Infisical, head to Organization Settings > Security and click **Connect** for SAML under the Connect to an Identity Provider section. Select Okta, then click **Connect** again. - + In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Okta**, then click **Connect** again. + + ![SSO connect section](../../../images/sso/connect-saml.png) + Next, copy the **Single sign-on URL** and **Audience URI (SP Entity ID)** to use when configuring the Okta SAML 2.0 application. ![Okta SAML initial configuration](../../../images/sso/okta/init-config.png) diff --git a/docs/images/platform/external-syncs/github-org-sync-active.png b/docs/images/platform/external-syncs/github-org-sync-active.png index bb5ce1ca3..1137d7601 100644 Binary files a/docs/images/platform/external-syncs/github-org-sync-active.png and b/docs/images/platform/external-syncs/github-org-sync-active.png differ diff --git a/docs/images/platform/external-syncs/github-org-sync-config-modal.png b/docs/images/platform/external-syncs/github-org-sync-config-modal.png index b856048e3..d02cd4589 100644 Binary files a/docs/images/platform/external-syncs/github-org-sync-config-modal.png and b/docs/images/platform/external-syncs/github-org-sync-config-modal.png differ diff --git a/docs/images/platform/external-syncs/github-org-sync-section.png b/docs/images/platform/external-syncs/github-org-sync-section.png index dad1fa425..870b9d055 100644 Binary files a/docs/images/platform/external-syncs/github-org-sync-section.png and b/docs/images/platform/external-syncs/github-org-sync-section.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-ca-crl.png b/docs/images/platform/pki/subscriber/subscriber-ca-crl.png new file mode 100644 index 000000000..35f7dad65 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-ca-crl.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-create-2.png b/docs/images/platform/pki/subscriber/subscriber-create-2.png new file mode 100644 index 000000000..fdfa44d27 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-create-2.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-create.png b/docs/images/platform/pki/subscriber/subscriber-create.png new file mode 100644 index 000000000..8a4709ea3 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-create.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-issue-cert-2.png b/docs/images/platform/pki/subscriber/subscriber-issue-cert-2.png new file mode 100644 index 000000000..916c5aab9 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-issue-cert-2.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-issue-cert.png b/docs/images/platform/pki/subscriber/subscriber-issue-cert.png new file mode 100644 index 000000000..f96c7db28 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-issue-cert.png differ diff --git a/docs/images/platform/pki/subscriber/subscriber-revoke-cert.png b/docs/images/platform/pki/subscriber/subscriber-revoke-cert.png new file mode 100644 index 000000000..4601991c8 Binary files /dev/null and b/docs/images/platform/pki/subscriber/subscriber-revoke-cert.png differ diff --git a/docs/images/platform/scim/scim-enable-provisioning.png b/docs/images/platform/scim/scim-enable-provisioning.png index a4385244f..37fc658b5 100644 Binary files a/docs/images/platform/scim/scim-enable-provisioning.png and b/docs/images/platform/scim/scim-enable-provisioning.png differ diff --git a/docs/images/platform/scim/scim-group-mapping.png b/docs/images/platform/scim/scim-group-mapping.png index 76baa8d8d..37bfcf45a 100644 Binary files a/docs/images/platform/scim/scim-group-mapping.png and b/docs/images/platform/scim/scim-group-mapping.png differ diff --git a/docs/images/sso/auth0-oidc/org-oidc-overview.png b/docs/images/sso/auth0-oidc/org-oidc-overview.png deleted file mode 100644 index f5778b97a..000000000 Binary files a/docs/images/sso/auth0-oidc/org-oidc-overview.png and /dev/null differ diff --git a/docs/images/sso/connect-ldap.png b/docs/images/sso/connect-ldap.png new file mode 100644 index 000000000..419d6f8b7 Binary files /dev/null and b/docs/images/sso/connect-ldap.png differ diff --git a/docs/images/sso/connect-oidc.png b/docs/images/sso/connect-oidc.png new file mode 100644 index 000000000..43da1bb0a Binary files /dev/null and b/docs/images/sso/connect-oidc.png differ diff --git a/docs/images/sso/connect-saml.png b/docs/images/sso/connect-saml.png new file mode 100644 index 000000000..40de3a0d2 Binary files /dev/null and b/docs/images/sso/connect-saml.png differ diff --git a/docs/images/sso/general-oidc/org-oidc-manage.png b/docs/images/sso/general-oidc/org-oidc-manage.png deleted file mode 100644 index f5778b97a..000000000 Binary files a/docs/images/sso/general-oidc/org-oidc-manage.png and /dev/null differ diff --git a/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png b/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png index 199a7432a..d3b38c762 100644 Binary files a/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png and b/docs/images/sso/keycloak-oidc/group-membership-mapping/enable-group-membership-mapping.png differ diff --git a/docs/images/sso/keycloak-oidc/manage-org-oidc.png b/docs/images/sso/keycloak-oidc/manage-org-oidc.png deleted file mode 100644 index f5778b97a..000000000 Binary files a/docs/images/sso/keycloak-oidc/manage-org-oidc.png and /dev/null differ diff --git a/docs/mint.json b/docs/mint.json index 21c7ae45c..46075033a 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -112,6 +112,7 @@ "pages": [ "documentation/platform/pki/overview", "documentation/platform/pki/private-ca", + "documentation/platform/pki/subscribers", "documentation/platform/pki/certificates", "documentation/platform/pki/pki-issuer", "documentation/platform/pki/est", @@ -1485,6 +1486,18 @@ { "group": "Infisical PKI", "pages": [ + { + "group": "Subscribers", + "pages": [ + "api-reference/endpoints/pki/subscribers/list-certs", + "api-reference/endpoints/pki/subscribers/create", + "api-reference/endpoints/pki/subscribers/read", + "api-reference/endpoints/pki/subscribers/update", + "api-reference/endpoints/pki/subscribers/delete", + "api-reference/endpoints/pki/subscribers/issue-cert", + "api-reference/endpoints/pki/subscribers/sign-cert" + ] + }, { "group": "Certificate Authorities", "pages": [ diff --git a/frontend/public/lotties/check.json b/frontend/public/lotties/check.json new file mode 100644 index 000000000..8d66090dc --- /dev/null +++ b/frontend/public/lotties/check.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":500,"h":500,"nm":"system-regular-31-check","ddd":0,"assets":[{"id":"comp_1","nm":"hover-check","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":[253.419,260.347,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":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[149.956,-122.947],[-31.321,57.362],[-83.54,5.208]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - 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]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[28.5]},{"t":20,"s":[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":"st","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').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":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":0,"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":[253.419,260.347,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":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[149.956,-122.947],[-31.321,57.362],[-83.54,5.208]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.05],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":21,"s":[0]},{"t":60,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.05],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":21,"s":[0]},{"t":60,"s":[28.5]}],"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":"st","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').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":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"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":0,"k":-180,"ix":10},"p":{"a":0,"k":[250.004,250.003,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":0,"k":{"i":[[-2.572,-106.399],[106.399,-2.572],[2.572,106.399],[-106.399,2.572]],"o":[[2.572,106.399],[-106.399,2.572],[-2.572,-106.399],[106.399,-2.572]],"v":[[192.652,-4.656],[4.656,192.652],[-192.652,4.656],[-4.656,-192.652]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').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":1,"ct":1,"bm":0},{"ddd":0,"ind":4,"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,249.974,0],"ix":2,"l":2},"a":{"a":0,"k":[250,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.57,-1.64],[2.27,-0.05],[1.65,1.56],[0.05,2.27],[-4.68,0.11],[-0.07,0],[-1.6,-1.52],[-0.05,-2.27]],"o":[[-1.57,1.64],[-2.28,0.06],[-1.65,-1.56],[-0.11,-4.69],[0.07,0],[2.19,0],[1.64,1.57],[0.06,2.26]],"v":[[6.15,5.861],[0.2,8.491],[-5.87,6.151],[-8.5,0.201],[-0.21,-8.499],[0,-8.499],[5.86,-6.149],[8.49,-0.199]],"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":[[2.67,-0.06],[-0.13,-5.51],[-1.93,-1.84],[-2.58,0],[-0.08,0],[-1.84,1.93],[0.06,2.67],[1.93,1.84]],"o":[[-5.51,0.14],[0.06,2.67],[1.88,1.79],[0.08,0],[2.67,-0.06],[1.84,-1.93],[-0.06,-2.67],[-1.94,-1.84]],"v":[[-0.24,-9.999],[-10,0.241],[-6.9,7.241],[-0.01,10.001],[0.24,10.001],[7.24,6.901],[10,-0.239],[6.9,-7.239]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').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,249.999],"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":0,"k":{"i":[[0.3,-0.29],[0,0],[0,0],[0.29,-0.29],[-0.29,-0.29],[0,0],[-0.19,0],[-0.15,0.15],[0,0],[0.3,0.3]],"o":[[0,0],[0,0],[-0.29,-0.29],[-0.29,0.29],[0,0],[0.15,0.15],[0.19,0],[0,0],[0.3,-0.29],[-0.29,-0.29]],"v":[[3.476,-3.286],[-1.504,1.694],[-3.484,-0.276],[-4.544,-0.276],[-4.544,0.784],[-2.034,3.284],[-1.504,3.504],[-0.974,3.284],[4.536,-2.226],[4.536,-3.286]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').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.164,250.496],"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":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"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,249.974,0],"ix":2,"l":2},"a":{"a":0,"k":[250,249.999,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.57,-1.64],[2.27,-0.05],[1.65,1.56],[0.05,2.27],[-4.68,0.11],[-0.07,0],[-1.6,-1.52],[-0.05,-2.27]],"o":[[-1.57,1.64],[-2.28,0.06],[-1.65,-1.56],[-0.11,-4.69],[0.07,0],[2.19,0],[1.64,1.57],[0.06,2.26]],"v":[[6.15,5.861],[0.2,8.491],[-5.87,6.151],[-8.5,0.201],[-0.21,-8.499],[0,-8.499],[5.86,-6.149],[8.49,-0.199]],"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":[[2.67,-0.06],[-0.13,-5.51],[-1.93,-1.84],[-2.58,0],[-0.08,0],[-1.84,1.93],[0.06,2.67],[1.93,1.84]],"o":[[-5.51,0.14],[0.06,2.67],[1.88,1.79],[0.08,0],[2.67,-0.06],[1.84,-1.93],[-0.06,-2.67],[-1.94,-1.84]],"v":[[-0.24,-9.999],[-10,0.241],[-6.9,7.241],[-0.01,10.001],[0.24,10.001],[7.24,6.901],[10,-0.239],[6.9,-7.239]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').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,249.999],"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":0,"k":{"i":[[0.3,-0.29],[0,0],[0,0],[0.29,-0.29],[-0.29,-0.29],[0,0],[-0.19,0],[-0.15,0.15],[0,0],[0.3,0.3]],"o":[[0,0],[0,0],[-0.29,-0.29],[-0.29,0.29],[0,0],[0.15,0.15],[0.19,0],[0,0],[0.3,-0.29],[-0.29,-0.29]],"v":[[3.476,-3.286],[-1.504,1.694],[-3.484,-0.276],[-4.544,-0.276],[-4.544,0.784],[-2.034,3.284],[-1.504,3.504],[-0.974,3.284],[4.536,-2.226],[4.536,-3.286]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.91,0.91,0.914,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-31-check').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.164,250.496],"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":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"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":[0.91,0.91,0.914],"ix":1}}]}],"ip":0,"op":302,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-check","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-check","dr":60}],"props":{}} \ No newline at end of file diff --git a/frontend/public/lotties/pki-subscriber.json b/frontend/public/lotties/pki-subscriber.json new file mode 100644 index 000000000..f6e0ce16e --- /dev/null +++ b/frontend/public/lotties/pki-subscriber.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":89,"w":430,"h":430,"nm":"wired-outline-88-document-user","ddd":0,"assets":[{"id":"comp_1","nm":"Content-12","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 4","parent":2,"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.41,"y":0},"t":6,"s":[0.044,-73.171,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[0.044,-117.966,0],"to":[0,0,0],"ti":[0,0,0]},{"t":50,"s":[0.044,-100.171,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":0,"k":{"i":[[0,22.108],[22.108,0],[0,-22.108],[-22.108,0]],"o":[[0,-22.108],[-22.108,0],[0,22.108],[22.108,0]],"v":[[40.03,0],[0,-40.03],[-40.03,0],[0,40.03]],"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('wired-outline-88-document-user').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"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":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.14,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[214.956,575.075,0],"to":[0,0,0],"ti":[0,0,0]},{"t":29,"s":[214.956,315.075,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":0,"k":{"i":[[0,0],[0,0],[0,0],[-30.376,0],[0,0],[0,-30.376]],"o":[[0,0],[0,0],[0,-30.376],[0,0],[30.376,0],[0,0]],"v":[[80.015,33.358],[-80.015,33.358],[-80.015,21.642],[-25.015,-33.358],[25.015,-33.358],[80.015,21.642]],"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('wired-outline-88-document-user').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"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":0,"op":844,"st":0,"ct":1,"bm":0}]},{"id":"comp_3","nm":"Content-36","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"outline 4","parent":2,"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.41,"y":0},"t":6,"s":[0.044,-73.171,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35,"s":[0.044,-117.966,0],"to":[0,0,0],"ti":[0,0,0]},{"t":50,"s":[0.044,-100.171,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":0,"k":{"i":[[0,22.108],[22.108,0],[0,-22.108],[-22.108,0]],"o":[[0,-22.108],[-22.108,0],[0,22.108],[22.108,0]],"v":[[40.03,0],[0,-40.03],[-40.03,0],[0,40.03]],"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('wired-outline-88-document-user').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"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":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"outline 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.14,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[214.956,575.075,0],"to":[0,0,0],"ti":[0,0,0]},{"t":29,"s":[214.956,315.075,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":0,"k":{"i":[[0,0],[0,0],[0,0],[-30.376,0],[0,0],[0,-30.376]],"o":[[0,0],[0,0],[0,-30.376],[0,0],[30.376,0],[0,0]],"v":[[80.015,33.358],[-80.015,33.358],[-80.015,21.642],[-25.015,-33.358],[25.015,-33.358],[80.015,21.642]],"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('wired-outline-88-document-user').layer('control').effect('secondary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".secondary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"secondary"},{"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":0,"op":844,"st":0,"ct":1,"bm":0}]},{"id":"comp_4","nm":"hover-swipe","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Page-corner","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.001,249.76,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,249.76,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.22,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[{"i":[[0,0],[-49.694,-50.431],[0,0]],"o":[[0,0],[50.313,51.06],[0,0]],"v":[[-53.373,-53.373],[-0.373,-0.627],[53.373,53.373]],"c":false}]},{"t":89,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-53.373,-53.373],[-53.373,53.373],[53.373,53.373]],"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('wired-outline-88-document-user').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[330.06,116.567],"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":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Page","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.243],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.326],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[9]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":47,"s":[-7]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":70,"s":[5]},{"t":89,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.243,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[317.001,368.76,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.326,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[351.001,381.76,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[291.751,356.51,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":65,"s":[321.001,369.26,0],"to":[0,0,0],"ti":[0,0,0]},{"t":80,"s":[317.001,368.76,0]}],"ix":2,"l":2},"a":{"a":0,"k":[352.001,403.76,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.833,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-53.373,-53.373],[-53.373,53.373],[53.373,53.373]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-53.373,-53.373],[-53.373,53.373],[53.373,53.373]],"c":false}]},{"t":38,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-213.237,-53.373],[-213.237,319.57],[53.373,319.57]],"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('wired-outline-88-document-user').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[330.06,116.567],"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":1,"k":[{"t":20,"s":[100],"h":1},{"t":38,"s":[0],"h":1}],"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],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[26.69,-186.57],[-133.43,-186.57],[-133.43,186.57],[133.43,186.57],[133.43,-79.82]],"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('wired-outline-88-document-user').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5,"x":"var $bm_rt;\n$bm_rt = $bm_mul($bm_div(value, 3), comp('wired-outline-88-document-user').layer('control').effect('stroke')('Menu'));"},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250,249.76],"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":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":844,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"mask","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[249.001,249.76,0],"ix":2,"l":2},"a":{"a":0,"k":[250.001,249.76,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":20,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[26.69,-186.57],[26.75,-186.57],[26.75,-79.76],[133.43,-79.76],[133.43,-79.82]],"c":true}]},{"t":38,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[26.69,-186.57],[-133.43,-186.57],[-133.43,186.57],[133.43,186.57],[133.43,-79.82]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250,249.76],"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":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":51,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"Content-12","parent":2,"tt":2,"tp":3,"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":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":51,"st":-50,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"Content-12","parent":2,"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":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[348.631,28.403],[82.211,28.403],[82.211,401.557],[348.631,401.557]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"w":430,"h":430,"ip":37.5,"op":881.5,"st":37.5,"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":"stroke","np":3,"mn":"Pseudo/@@jxAy4KF1Sn6X4aYQ0vVH/w","ix":1,"en":1,"ef":[{"ty":7,"nm":"Menu","mn":"Pseudo/@@jxAy4KF1Sn6X4aYQ0vVH/w-0001","ix":1,"v":{"a":0,"k":3,"ix":1}}]},{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":2,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]},{"ty":5,"nm":"secondary","np":3,"mn":"ADBE Color Control","ix":3,"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":360,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"hover-swipe","refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[215,215,0],"ix":2,"l":2},"a":{"a":0,"k":[215,215,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":430,"h":430,"ip":0,"op":99,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-swipe","dr":89}],"props":{}} \ No newline at end of file diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 5efe7ca69..e390fbcc6 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -23,6 +23,10 @@ export const ROUTE_PATHS = Object.freeze({ "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback" ) }, + SsoPage: setRoute( + "/organization/sso", + "/_authenticate/_inject-org-details/_org-layout/organization/sso" + ), SecretScanning: setRoute( "/organization/secret-scanning", "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning" @@ -283,9 +287,13 @@ export const ROUTE_PATHS = Object.freeze({ "/cert-manager/$projectId/ca/$caId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId" ), - OverviewPage: setRoute( - "/cert-manager/$projectId/overview", - "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview" + SubscribersPage: setRoute( + "/cert-manager/$projectId/subscribers", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers" + ), + CertificatesPage: setRoute( + "/cert-manager/$projectId/certificates", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates" ), CertificateAuthoritiesPage: setRoute( "/cert-manager/$projectId/certificate-authorities", @@ -298,6 +306,10 @@ export const ROUTE_PATHS = Object.freeze({ PkiCollectionDetailsByIDPage: setRoute( "/cert-manager/$projectId/pki-collections/$collectionId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId" + ), + PkiSubscriberDetailsByIDPage: setRoute( + "/cert-manager/$projectId/subscribers/$subscriberName", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName" ) }, Ssh: { diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index b195571f8..d7b7334ea 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -9,5 +9,7 @@ export { ProjectPermissionIdentityActions, ProjectPermissionKmipActions, ProjectPermissionMemberActions, + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSshHostActions, ProjectPermissionSub } from "./types"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d1a257653..a640f6eaa 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -95,6 +95,15 @@ export enum ProjectPermissionSshHostActions { IssueHostCert = "issue-host-cert" } +export enum ProjectPermissionPkiSubscriberActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + IssueCert = "issue-cert", + ListCerts = "list-certs" +} + export enum ProjectPermissionSecretRotationActions { Read = "read", ReadGeneratedCredentials = "read-generated-credentials", @@ -186,6 +195,7 @@ export enum ProjectPermissionSub { SshHostGroups = "ssh-host-groups", PkiAlerts = "pki-alerts", PkiCollections = "pki-collections", + PkiSubscribers = "pki-subscribers", Kms = "kms", Cmek = "cmek", SecretSyncs = "secret-syncs", @@ -220,6 +230,14 @@ export type SecretRotationSubjectFields = { secretPath: string; }; +export type SshHostSubjectFields = { + hostname: string; +}; + +export type PkiSubscriberSubjectFields = { + name: string; +}; + export type ProjectPermissionSet = | [ ProjectPermissionSecretActions, @@ -282,7 +300,20 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups] - | [ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts] + | [ + ProjectPermissionSshHostActions, + ( + | ProjectPermissionSub.SshHosts + | (ForcedSubject & SshHostSubjectFields) + ) + ] + | [ + ProjectPermissionPkiSubscriberActions, + ( + | ProjectPermissionSub.PkiSubscribers + | (ForcedSubject & PkiSubscriberSubjectFields) + ) + ] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index 04af3c8a4..91fcd9055 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -17,6 +17,8 @@ export { ProjectPermissionIdentityActions, ProjectPermissionKmipActions, ProjectPermissionMemberActions, + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSshHostActions, ProjectPermissionSub, useProjectPermission } from "./ProjectPermissionContext"; diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index cc5c6fa91..3e0d0f52e 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -61,6 +61,9 @@ export const initProjectHelper = async ({ projectName }: { projectName: string } return project; }; export const getProjectHomePage = (workspace: Workspace) => { + if (workspace.type === ProjectType.CertificateManager) { + return `/${workspace.type}/$projectId/subscribers` as const; + } return `/${workspace.type}/$projectId/overview` as const; }; diff --git a/frontend/src/hooks/api/certificates/mutations.tsx b/frontend/src/hooks/api/certificates/mutations.tsx index 7e9cf4f91..74d6b5cbb 100644 --- a/frontend/src/hooks/api/certificates/mutations.tsx +++ b/frontend/src/hooks/api/certificates/mutations.tsx @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { pkiSubscriberKeys } from "../pkiSubscriber/queries"; import { workspaceKeys } from "../workspace"; import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types"; @@ -42,6 +43,9 @@ export const useRevokeCert = () => { queryClient.invalidateQueries({ queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug) }); + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates() + }); } }); }; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 4bc06f7e3..4b4967f16 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -27,6 +27,7 @@ export * from "./orgAdmin"; export * from "./organization"; export * from "./pkiAlerts"; export * from "./pkiCollections"; +export * from "./pkiSubscriber"; export * from "./projectUserAdditionalPrivilege"; export * from "./rateLimit"; export * from "./roles"; diff --git a/frontend/src/hooks/api/pkiSubscriber/constants.tsx b/frontend/src/hooks/api/pkiSubscriber/constants.tsx new file mode 100644 index 000000000..1de5e9ddb --- /dev/null +++ b/frontend/src/hooks/api/pkiSubscriber/constants.tsx @@ -0,0 +1,20 @@ +export enum PkiSubscriberStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export const pkiSubscriberStatusToNameMap: { [K in PkiSubscriberStatus]: string } = { + [PkiSubscriberStatus.ACTIVE]: "Active", + [PkiSubscriberStatus.DISABLED]: "Disabled" +}; + +export const getPkiSubscriberStatusBadgeVariant = (status: PkiSubscriberStatus) => { + switch (status) { + case PkiSubscriberStatus.ACTIVE: + return "success"; + case PkiSubscriberStatus.DISABLED: + return "danger"; + default: + return "primary"; + } +}; diff --git a/frontend/src/hooks/api/pkiSubscriber/index.tsx b/frontend/src/hooks/api/pkiSubscriber/index.tsx new file mode 100644 index 000000000..b086839df --- /dev/null +++ b/frontend/src/hooks/api/pkiSubscriber/index.tsx @@ -0,0 +1,7 @@ +export { + useCreatePkiSubscriber, + useDeletePkiSubscriber, + useIssuePkiSubscriberCert, + useUpdatePkiSubscriber +} from "./mutations"; +export { useGetPkiSubscriber, useGetPkiSubscriberCertificates } from "./queries"; diff --git a/frontend/src/hooks/api/pkiSubscriber/mutations.tsx b/frontend/src/hooks/api/pkiSubscriber/mutations.tsx new file mode 100644 index 000000000..a7d0eef92 --- /dev/null +++ b/frontend/src/hooks/api/pkiSubscriber/mutations.tsx @@ -0,0 +1,110 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TCreateCertificateResponse } from "../ca/types"; +import { workspaceKeys } from "../workspace/query-keys"; +import { pkiSubscriberKeys } from "./queries"; +import { + TCreatePkiSubscriberDTO, + TDeletePkiSubscriberDTO, + TIssuePkiSubscriberCertDTO, + TPkiSubscriber, + TUpdatePkiSubscriberDTO +} from "./types"; + +export const useCreatePkiSubscriber = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { data: subscriber } = await apiRequest.post("/api/v1/pki/subscribers", body); + return subscriber; + }, + onSuccess: ({ projectId, name }) => { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId) + }); + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.getPkiSubscriber({ + subscriberName: name, + projectId + }) + }); + } + }); +}; + +export const useUpdatePkiSubscriber = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ subscriberName, ...body }) => { + const { data: subscriber } = await apiRequest.patch( + `/api/v1/pki/subscribers/${subscriberName}`, + body + ); + return subscriber; + }, + onSuccess: ({ projectId, name }) => { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId) + }); + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.getPkiSubscriber({ + subscriberName: name, + projectId + }) + }); + } + }); +}; + +export const useDeletePkiSubscriber = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ subscriberName, projectId }) => { + const { data: subscriber } = await apiRequest.delete( + `/api/v1/pki/subscribers/${subscriberName}`, + { + data: { + projectId + } + } + ); + return subscriber; + }, + onSuccess: ({ name, projectId }) => { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId) + }); + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.getPkiSubscriber({ + subscriberName: name, + projectId + }) + }); + } + }); +}; + +export const useIssuePkiSubscriberCert = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ subscriberName, projectId }) => { + const { data } = await apiRequest.post( + `/api/v1/pki/subscribers/${subscriberName}/issue-certificate`, + { + projectId + } + ); + return data; + }, + onSuccess: (_, { subscriberName, projectId }) => { + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.forPkiSubscriberCertificates({ + subscriberName, + projectId + }) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/pkiSubscriber/queries.tsx b/frontend/src/hooks/api/pkiSubscriber/queries.tsx new file mode 100644 index 000000000..d9948ed5c --- /dev/null +++ b/frontend/src/hooks/api/pkiSubscriber/queries.tsx @@ -0,0 +1,102 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TCertificate } from "../certificates/types"; +import { TPkiSubscriber } from "./types"; + +export const pkiSubscriberKeys = { + getPkiSubscriber: ({ + subscriberName, + projectId + }: { + subscriberName: string; + projectId: string; + }) => [{ subscriberName, projectId }, "pki-subscriber"] as const, + allPkiSubscriberCertificates: () => ["pki-subscriber-certificates"] as const, + forPkiSubscriberCertificates: ({ + subscriberName, + projectId + }: { + subscriberName: string; + projectId: string; + }) => [...pkiSubscriberKeys.allPkiSubscriberCertificates(), subscriberName, projectId] as const, + specificPkiSubscriberCertificates: ({ + subscriberName, + projectId, + offset, + limit + }: { + subscriberName: string; + projectId: string; + offset: number; + limit: number; + }) => + [ + ...pkiSubscriberKeys.forPkiSubscriberCertificates({ subscriberName, projectId }), + { offset, limit, projectId } + ] as const +}; + +export const useGetPkiSubscriber = ({ + subscriberName, + projectId +}: { + subscriberName: string; + projectId: string; +}) => { + return useQuery({ + queryKey: pkiSubscriberKeys.getPkiSubscriber({ subscriberName, projectId }), + queryFn: async () => { + const { data: pkiSubscriber } = await apiRequest.get( + `/api/v1/pki/subscribers/${subscriberName}`, + { + params: { + projectId + } + } + ); + return pkiSubscriber; + }, + enabled: Boolean(subscriberName) && Boolean(projectId) + }); +}; + +export const useGetPkiSubscriberCertificates = ({ + subscriberName, + projectId, + offset, + limit +}: { + subscriberName: string; + projectId: string; + offset: number; + limit: number; +}) => { + return useQuery({ + queryKey: pkiSubscriberKeys.specificPkiSubscriberCertificates({ + subscriberName, + projectId, + offset, + limit + }), + queryFn: async () => { + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit), + projectId + }); + + const { + data: { certificates, totalCount } + } = await apiRequest.get<{ certificates: TCertificate[]; totalCount: number }>( + `/api/v1/pki/subscribers/${subscriberName}/certificates`, + { + params + } + ); + return { certificates, totalCount }; + }, + enabled: Boolean(subscriberName) && Boolean(projectId) + }); +}; diff --git a/frontend/src/hooks/api/pkiSubscriber/types.ts b/frontend/src/hooks/api/pkiSubscriber/types.ts new file mode 100644 index 000000000..e6050dd13 --- /dev/null +++ b/frontend/src/hooks/api/pkiSubscriber/types.ts @@ -0,0 +1,53 @@ +import { CertExtendedKeyUsage, CertKeyUsage } from "../certificates/enums"; + +export enum PkiSubscriberStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export type TPkiSubscriber = { + id: string; + projectId: string; + caId: string; + name: string; + commonName: string; + status: PkiSubscriberStatus; + ttl: string; + subjectAlternativeNames: string[]; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; +}; + +export type TCreatePkiSubscriberDTO = { + projectId: string; + caId: string; + name: string; + commonName: string; + ttl: string; + subjectAlternativeNames: string[]; + keyUsages: CertKeyUsage[]; + extendedKeyUsages: CertExtendedKeyUsage[]; +}; + +export type TUpdatePkiSubscriberDTO = { + subscriberName: string; + projectId: string; + caId?: string; + name?: string; + commonName?: string; + status?: PkiSubscriberStatus; + ttl?: string; + subjectAlternativeNames?: string[]; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; +}; + +export type TDeletePkiSubscriberDTO = { + subscriberName: string; + projectId: string; +}; + +export type TIssuePkiSubscriberCertDTO = { + subscriberName: string; + projectId: string; +}; diff --git a/frontend/src/hooks/api/sshHost/types.ts b/frontend/src/hooks/api/sshHost/types.ts index ff33664b1..ba44bdde3 100644 --- a/frontend/src/hooks/api/sshHost/types.ts +++ b/frontend/src/hooks/api/sshHost/types.ts @@ -21,6 +21,7 @@ export type TSshHost = { hostCertTtl: string; loginMappings: TLoginMapping[]; }; + export type TCreateSshHostDTO = { projectId: string; hostname: string; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index b841f4bff..df2d55dc3 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -35,6 +35,7 @@ export { useListWorkspaceGroups, useListWorkspacePkiAlerts, useListWorkspacePkiCollections, + useListWorkspacePkiSubscribers, useListWorkspaceSshCas, useListWorkspaceSshCertificates, useListWorkspaceSshCertificateTemplates, diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 441b9aefa..278b62bc8 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -14,6 +14,7 @@ import { IntegrationAuth } from "../integrationAuth/types"; import { TIntegration } from "../integrations/types"; import { TPkiAlert } from "../pkiAlerts/types"; import { TPkiCollection } from "../pkiCollections/types"; +import { TPkiSubscriber } from "../pkiSubscriber/types"; import { EncryptedSecret } from "../secrets/types"; import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types"; import { TSshCertificateTemplate } from "../sshCertificateTemplates/types"; @@ -874,6 +875,21 @@ export const useListWorkspaceSshHosts = (projectId: string) => { }); }; +export const useListWorkspacePkiSubscribers = (projectId: string) => { + return useQuery({ + queryKey: workspaceKeys.getWorkspacePkiSubscribers(projectId), + queryFn: async () => { + const { + data: { subscribers } + } = await apiRequest.get<{ subscribers: TPkiSubscriber[] }>( + `/api/v2/workspace/${projectId}/pki-subscribers` + ); + return subscribers; + }, + enabled: Boolean(projectId) + }); +}; + export const useListWorkspaceSshHostGroups = (projectId: string) => { return useQuery({ queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId), diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index 335248a80..c10616b63 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -54,6 +54,8 @@ export const workspaceKeys = { }) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const, getWorkspacePkiAlerts: (workspaceId: string) => [{ workspaceId }, "workspace-pki-alerts"] as const, + getWorkspacePkiSubscribers: (projectId: string) => + [{ projectId }, "workspace-pki-subscribers"] as const, getWorkspacePkiCollections: (workspaceId: string) => [{ workspaceId }, "workspace-pki-collections"] as const, getWorkspaceCertificateTemplates: (workspaceId: string) => diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx index 054a873a3..4d55edb58 100644 --- a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx @@ -5,22 +5,6 @@ import { Menu, MenuGroup, MenuItem } from "@app/components/v2"; export const DefaultSideBar = () => ( - - {({ isActive }) => ( - - Audit Logs - - )} - - - {({ isActive }) => ( - - Usage & Billing - - )} - - - {({ isActive }) => ( @@ -42,6 +26,29 @@ export const DefaultSideBar = () => ( )} + + {({ isActive }) => ( + + Single Sign-On (SSO) + + )} + + + + + {({ isActive }) => ( + + Audit Logs + + )} + + + {({ isActive }) => ( + + Usage & Billing + + )} + {({ isActive }) => ( diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index 8cf807d56..b85499073 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -4,6 +4,7 @@ import { faArrowUpRightFromSquare, faBook, faCheck, + faCheckCircle, faCog, faDoorClosed, faEnvelope, @@ -118,6 +119,9 @@ export const MinimizedOrgSidebar = () => { [ linkOptions({ to: "/organization/access-management" }).to, linkOptions({ to: "/organization/app-connections" }).to, + linkOptions({ to: "/organization/billing" }).to, + linkOptions({ to: "/organization/sso" }).to, + linkOptions({ to: "/organization/gateways" }).to, linkOptions({ to: "/organization/settings" }).to, linkOptions({ to: "/organization/audit-logs" }).to ] as string[] @@ -387,6 +391,13 @@ export const MinimizedOrgSidebar = () => { Audit Logs + + } + > + SSO Settings + + }> Organization Settings diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index 8533d7007..35534d4a2 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -104,7 +104,23 @@ export const ProjectLayout = () => { {isCertManager && ( <> + {({ isActive }) => ( + + Subscribers + + )} + + { {t("common.head-title", { title: "Alerting" })}
- + { handlePopUpClose("deleteCa"); navigate({ - to: `/${ProjectType.CertificateManager}/$projectId/overview` as const, + to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const, params: { projectId } diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx index f74ececaf..0efe2dbdc 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx @@ -15,7 +15,10 @@ export const CertificateAuthoritiesPage = () => { {t("common.head-title", { title: "Certificate Authorities" })}
- + { {t("common.head-title", { title: "Certificates" })}
- + {/* If both are false, the section does not render. This is to prevent duplicate banners. */} {(canAccessCerts || canAccessPkiColl) && ( { - + @@ -85,7 +85,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter); return ( - +
Friendly NameCommon Name Status Not Before Not After
{certificate.friendlyName}{certificate.commonName} {certificate.status === CertStatus.REVOKED ? ( Revoked diff --git a/frontend/src/pages/cert-manager/CertificatesPage/route.tsx b/frontend/src/pages/cert-manager/CertificatesPage/route.tsx index 431812886..0bb7e7a71 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/route.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { CertificatesPage } from "./CertificatesPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview" + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates" )({ component: CertificatesPage }); diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx index 073cac6e8..e851e33d3 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx @@ -57,7 +57,7 @@ export const PkiCollectionPage = () => { }); handlePopUpClose("deletePkiCollection"); navigate({ - to: `/${ProjectType.CertificateManager}/$projectId/overview` as const, + to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const, params: { projectId } diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx index d59ebd265..e1ff5c1e7 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/routes.tsx @@ -13,7 +13,7 @@ export const Route = createFileRoute( { label: "Certificate Collections", link: linkOptions({ - to: "/cert-manager/$projectId/overview", + to: "/cert-manager/$projectId/certificates", params: { projectId: params.projectId } diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx new file mode 100644 index 000000000..ad6e88adf --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx @@ -0,0 +1,165 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + PageHeader, + Tooltip +} from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; +import { useDeletePkiSubscriber, useGetPkiSubscriber } from "@app/hooks/api"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { PkiSubscriberModal } from "../PkiSubscribersPage/components/PkiSubscriberModal"; +import { PkiSubscriberCertificatesSection, PkiSubscriberDetailsSection } from "./components"; + +const Page = () => { + const navigate = useNavigate(); + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const subscriberName = useParams({ + from: ROUTE_PATHS.CertManager.PkiSubscriberDetailsByIDPage.id, + select: (el) => el.subscriberName + }); + const { data } = useGetPkiSubscriber({ + subscriberName, + projectId + }); + + const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "pkiSubscriber", + "deletePkiSubscriber" + ] as const); + + const onRemoveSubscriberSubmit = async (subscriberNameToDelete: string) => { + try { + if (!projectId) return; + + await deletePkiSubscriber({ subscriberName: subscriberNameToDelete, projectId }); + + createNotification({ + text: "Successfully deleted subscriber", + type: "success" + }); + + handlePopUpClose("deletePkiSubscriber"); + navigate({ + to: `/${ProjectType.CertificateManager}/$projectId/subscribers` as const, + params: { + projectId + } + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete subscriber", + type: "error" + }); + } + }; + + return ( +
+ {data && ( +
+ + + +
+ + + +
+
+ + + {(isAllowed) => ( + + handlePopUpOpen("deletePkiSubscriber", { + subscriberName: data.name + }) + } + disabled={!isAllowed} + > + Delete PKI Subscriber + + )} + + +
+
+
+
+ +
+
+ +
+
+
+ )} + + handlePopUpToggle("deletePkiSubscriber", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemoveSubscriberSubmit( + (popUp?.deletePkiSubscriber?.data as { subscriberName: string })?.subscriberName + ) + } + /> +
+ ); +}; + +export const PkiSubscriberDetailsByIDPage = () => { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "PKI Subscriber" })} + + + + + + ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesSection.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesSection.tsx new file mode 100644 index 000000000..2e486cf85 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesSection.tsx @@ -0,0 +1,27 @@ +import { usePopUp } from "@app/hooks"; +import { CertificateRevocationModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal"; + +import { PkiSubscriberCertificatesTable } from "./PkiSubscriberCertificatesTable"; + +type Props = { + subscriberName: string; +}; + +export const PkiSubscriberCertificatesSection = ({ subscriberName }: Props) => { + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["revokeCertificate"] as const); + + return ( +
+
+

Certificates

+
+
+ +
+ +
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx new file mode 100644 index 000000000..e6e49b758 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx @@ -0,0 +1,176 @@ +import { useState } from "react"; +import { subject } from "@casl/ability"; +import { faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; +import { twMerge } from "tailwind-merge"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Badge, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { useGetPkiSubscriberCertificates } from "@app/hooks/api"; +import { CertStatus } from "@app/hooks/api/certificates/enums"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + subscriberName: string; + handlePopUpOpen?: (popUpName: keyof UsePopUpState<["revokeCertificate"]>, data?: object) => void; +}; + +const PER_PAGE_INIT = 25; + +export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen }: Props) => { + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const { permission } = useProjectPermission(); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(PER_PAGE_INIT); + + const { data, isPending } = useGetPkiSubscriberCertificates({ + subscriberName, + projectId, + offset: (page - 1) * perPage, + limit: perPage + }); + + const getCertStatusBadge = (status: string, notAfter: string) => { + if (status === CertStatus.REVOKED) { + return Revoked; + } + + const expiryDate = new Date(notAfter); + const now = new Date(); + const daysUntilExpiry = Math.floor( + (expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24) + ); + + if (daysUntilExpiry < 0) { + return Expired; + } + + if (daysUntilExpiry < 30) { + return Expiring Soon; + } + + return Valid; + }; + + const canListPkiSubscriberCerts = permission.can( + ProjectPermissionPkiSubscriberActions.ListCerts, + subject(ProjectPermissionSub.PkiSubscribers, { + name: subscriberName + }) + ); + + return ( +
+ + + + + + + + + + + + {isPending && } + {!isPending && + data?.certificates?.map((certificate) => { + return ( + + + + + + + + ); + })} + +
Common NameStatusNot BeforeNot After +
{certificate.commonName}{getCertStatusBadge(certificate.status, certificate.notAfter)} + {certificate.notBefore + ? format(new Date(certificate.notBefore), "yyyy-MM-dd") + : "-"} + + {certificate.notAfter + ? format(new Date(certificate.notAfter), "yyyy-MM-dd") + : "-"} + + + +
+ + + +
+
+ + + {(isAllowed) => ( + + handlePopUpOpen && + handlePopUpOpen("revokeCertificate", { + serialNumber: certificate.serialNumber + }) + } + disabled={!isAllowed} + icon={} + > + Revoke Certificate + + )} + + +
+
+ {!isPending && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {!isPending && !data?.certificates?.length && ( + + )} +
+
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx new file mode 100644 index 000000000..5899f5004 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberDetailsSection.tsx @@ -0,0 +1,190 @@ +import { useState } from "react"; +import { subject } from "@casl/ability"; +import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, IconButton, Modal, ModalContent, Tooltip } from "@app/components/v2"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { useTimedReset } from "@app/hooks"; +import { useGetPkiSubscriber, useIssuePkiSubscriberCert } from "@app/hooks/api"; +import { pkiSubscriberStatusToNameMap } from "@app/hooks/api/pkiSubscriber/constants"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { CertificateContent } from "../../CertificatesPage/components/CertificateContent"; + +type Props = { + subscriberName: string; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, data?: object) => void; +}; + +type TCertificateDetails = { + serialNumber: string; + certificate: string; + certificateChain: string; + privateKey: string; +}; + +export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }: Props) => { + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const { permission } = useProjectPermission(); + const [certificateDetails, setCertificateDetails] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + const { data: pkiSubscriber } = useGetPkiSubscriber({ + subscriberName, + projectId + }); + + const { mutateAsync: issuePkiSubscriberCert, isPending: isIssuingCert } = + useIssuePkiSubscriberCert(); + + const onIssuePkiSubscriberCert = async () => { + try { + const response = await issuePkiSubscriberCert({ subscriberName, projectId }); + + setCertificateDetails({ + serialNumber: response.serialNumber, + certificate: response.certificate, + certificateChain: response.certificateChain, + privateKey: response.privateKey + }); + + setIsModalOpen(true); + + createNotification({ + text: "Successfully issued certificate", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to issue certificate", + type: "error" + }); + } + }; + + const canIssuePkiSubscriberCert = permission.can( + ProjectPermissionPkiSubscriberActions.IssueCert, + subject(ProjectPermissionSub.PkiSubscribers, { + name: pkiSubscriber?.name ?? "" + }) + ); + + return pkiSubscriber ? ( +
+
+

PKI Subscriber Details

+ + {(isAllowed) => { + return ( + + { + e.stopPropagation(); + handlePopUpOpen("pkiSubscriber", { + subscriberName: pkiSubscriber.name + }); + }} + > + + + + ); + }} + +
+
+
+

PKI Subscriber ID

+
+

{pkiSubscriber.id}

+
+ + { + navigator.clipboard.writeText(pkiSubscriber.id); + setCopyTextId("Copied"); + }} + > + + + +
+
+
+
+

Name

+

{pkiSubscriber.name}

+
+
+

Status

+

+ {pkiSubscriberStatusToNameMap[pkiSubscriber.status]} +

+
+
+

Common Name

+

{pkiSubscriber.commonName}

+
+ {canIssuePkiSubscriberCert && ( + + )} +
+ + { + setIsModalOpen(isOpen); + if (!isOpen) { + setCertificateDetails(null); + } + }} + > + + {certificateDetails && ( + + )} + + +
+ ) : ( +
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/index.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/index.tsx new file mode 100644 index 000000000..4a671fdcd --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/index.tsx @@ -0,0 +1,2 @@ +export { PkiSubscriberCertificatesSection } from "./PkiSubscriberCertificatesSection"; +export { PkiSubscriberDetailsSection } from "./PkiSubscriberDetailsSection"; diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx new file mode 100644 index 000000000..bb902bc76 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx @@ -0,0 +1,25 @@ +import { createFileRoute, linkOptions } from "@tanstack/react-router"; + +import { PkiSubscriberDetailsByIDPage } from "./PkiSubscriberDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName" +)({ + component: PkiSubscriberDetailsByIDPage, + beforeLoad: ({ context, params }) => { + return { + breadcrumbs: [ + ...context.breadcrumbs, + { + label: "Subscribers", + link: linkOptions({ + to: "/cert-manager/$projectId/subscribers", + params: { + projectId: params.projectId + } + }) + } + ] + }; + } +}); diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/PkiSubscribersPage.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/PkiSubscribersPage.tsx new file mode 100644 index 000000000..c95e9490e --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/PkiSubscribersPage.tsx @@ -0,0 +1,28 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; + +import { PageHeader } from "@app/components/v2"; + +import { PkiSubscriberSection } from "./components"; + +export const PkiSubscribersPage = () => { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "PKI Subscribers" })} + +
+
+
+ + +
+
+
+ + ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx new file mode 100644 index 000000000..951d78225 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx @@ -0,0 +1,428 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + Checkbox, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + CaStatus, + useCreatePkiSubscriber, + useGetPkiSubscriber, + useListWorkspaceCas, + useListWorkspacePkiSubscribers, + useUpdatePkiSubscriber +} from "@app/hooks/api"; +import { + EXTENDED_KEY_USAGES_OPTIONS, + KEY_USAGES_OPTIONS +} from "@app/hooks/api/certificates/constants"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + popUp: UsePopUpState<["pkiSubscriber"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, state?: boolean) => void; +}; + +const schema = z + .object({ + name: z.string().trim().min(1, "Name is required"), + caId: z.string().min(1, "Issuing CA is required"), + commonName: z.string().trim().min(1, "Common Name is required"), + subjectAlternativeNames: z.string(), + ttl: z.string().trim(), + keyUsages: z.object({ + [CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(), + [CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(), + [CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(), + [CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(), + [CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(), + [CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(), + [CertKeyUsage.CRL_SIGN]: z.boolean().optional(), + [CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(), + [CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional() + }), + extendedKeyUsages: z.object({ + [CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(), + [CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(), + [CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(), + [CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(), + [CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(), + [CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional() + }) + }) + .required(); + +export type FormData = z.infer; + +export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => { + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const { data: subscribers } = useListWorkspacePkiSubscribers(projectId); + const { data: cas } = useListWorkspaceCas({ + projectSlug: currentWorkspace?.slug ?? "", + status: CaStatus.ACTIVE + }); + + const { data: pkiSubscriber } = useGetPkiSubscriber({ + subscriberName: + (popUp?.pkiSubscriber?.data as { subscriberName: string })?.subscriberName || "", + projectId + }); + + const { mutateAsync: createMutateAsync } = useCreatePkiSubscriber(); + const { mutateAsync: updateMutateAsync } = useUpdatePkiSubscriber(); + + const { + control, + handleSubmit, + reset, + setValue, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "", + caId: "", + commonName: "", + subjectAlternativeNames: "", + ttl: "", + keyUsages: { + [CertKeyUsage.DIGITAL_SIGNATURE]: true, + [CertKeyUsage.KEY_ENCIPHERMENT]: true + }, + extendedKeyUsages: {} + } + }); + + useEffect(() => { + if (pkiSubscriber) { + reset({ + name: pkiSubscriber.name, + caId: pkiSubscriber.caId || "", + commonName: pkiSubscriber.commonName, + subjectAlternativeNames: pkiSubscriber.subjectAlternativeNames.join(", ") || "", + ttl: pkiSubscriber.ttl || "", + keyUsages: Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true])), + extendedKeyUsages: Object.fromEntries( + (pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true]) + ) + }); + } else { + reset({ + name: "", + caId: "", + commonName: "", + subjectAlternativeNames: "", + ttl: "", + keyUsages: { + [CertKeyUsage.DIGITAL_SIGNATURE]: true, + [CertKeyUsage.KEY_ENCIPHERMENT]: true + }, + extendedKeyUsages: {} + }); + } + }, [pkiSubscriber, reset]); + + useEffect(() => { + if (cas?.length) { + setValue("caId", cas[0].id); + } + }, [cas, setValue]); + + const onFormSubmit = async ({ + name, + caId, + commonName, + subjectAlternativeNames, + ttl, + keyUsages, + extendedKeyUsages + }: FormData) => { + try { + if (!projectId) return; + + if (!caId) { + createNotification({ + text: "Please select an Issuing CA", + type: "error" + }); + return; + } + + // Check if there is already a different subscriber with the same name + const existingNames = + subscribers?.filter((s) => s.id !== pkiSubscriber?.id).map((s) => s.name) || []; + + if (existingNames.includes(name.trim())) { + createNotification({ + text: "A subscriber with this name already exists.", + type: "error" + }); + return; + } + + const keyUsagesList = Object.entries(keyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertKeyUsage); + + const extendedKeyUsagesList = Object.entries(extendedKeyUsages) + .filter(([, value]) => value) + .map(([key]) => key as CertExtendedKeyUsage); + + const subjectAlternativeNamesList = subjectAlternativeNames + .split(",") + .map((san) => san.trim()) + .filter(Boolean); + + if (pkiSubscriber) { + await updateMutateAsync({ + subscriberName: pkiSubscriber.name, + projectId, + name, + caId, + commonName, + subjectAlternativeNames: subjectAlternativeNamesList, + ttl, + keyUsages: keyUsagesList, + extendedKeyUsages: extendedKeyUsagesList + }); + } else { + await createMutateAsync({ + projectId, + name, + caId, + commonName, + subjectAlternativeNames: subjectAlternativeNamesList, + ttl, + keyUsages: keyUsagesList, + extendedKeyUsages: extendedKeyUsagesList + }); + } + + reset(); + handlePopUpToggle("pkiSubscriber", false); + + createNotification({ + text: `Successfully ${pkiSubscriber ? "updated" : "added"} PKI subscriber`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to ${pkiSubscriber ? "update" : "add"} PKI subscriber`, + type: "error" + }); + } + }; + + return ( + { + reset(); + handlePopUpToggle("pkiSubscriber", isOpen); + }} + > + +
+ {pkiSubscriber && ( + + + + )} + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + + + +
Key Usage
+
+ + { + return ( + +
+ {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> + { + return ( + +
+ {EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> +
+
+
+
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx new file mode 100644 index 000000000..f81636e49 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberSection.tsx @@ -0,0 +1,151 @@ +import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, DeleteActionModal } from "@app/components/v2"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; +import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api"; +import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { PkiSubscriberModal } from "./PkiSubscriberModal"; +import { PkiSubscribersTable } from "./PkiSubscribersTable"; + +export const PkiSubscriberSection = () => { + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace.id; + const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber(); + const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "pkiSubscriber", + "pkiSubscriberStatus", // enable / disable + "deletePkiSubscriber" + ] as const); + + const onRemovePkiSubscriberSubmit = async (subscriberName: string) => { + try { + const subscriber = await deletePkiSubscriber({ subscriberName, projectId }); + + createNotification({ + text: `Successfully deleted PKI subscriber: ${subscriber.name}`, + type: "success" + }); + + handlePopUpClose("deletePkiSubscriber"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete PKI subscriber", + type: "error" + }); + } + }; + + const onUpdatePkiSubscriberStatus = async ({ + subscriberName, + status + }: { + subscriberName: string; + status: PkiSubscriberStatus; + }) => { + try { + if (!currentWorkspace?.slug) return; + + await updatePkiSubscriber({ subscriberName, projectId, status }); + + createNotification({ + text: `Successfully ${status === PkiSubscriberStatus.ACTIVE ? "enabled" : "disabled"} subscriber`, + type: "success" + }); + + handlePopUpClose("pkiSubscriberStatus"); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to ${status === PkiSubscriberStatus.ACTIVE ? "enable" : "disable"} subscriber`, + type: "error" + }); + } + }; + + const subscriberStatusData = popUp?.pkiSubscriberStatus?.data as { + status: PkiSubscriberStatus; + subscriberName: string; + }; + + const isEnabling = subscriberStatusData?.status === PkiSubscriberStatus.ACTIVE; + const subscriberName = subscriberStatusData?.subscriberName || ""; + + return ( +
+
+

Subscribers

+
+ + + Documentation{" "} + + + + + {(isAllowed) => ( + + )} + +
+
+ + + handlePopUpToggle("pkiSubscriberStatus", isOpen)} + deleteKey="confirm" + buttonColorSchema={isEnabling ? "primary" : "danger"} + buttonText={isEnabling ? "Enable" : "Disable"} + onDeleteApproved={() => onUpdatePkiSubscriberStatus(subscriberStatusData)} + /> + handlePopUpToggle("deletePkiSubscriber", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemovePkiSubscriberSubmit( + (popUp?.deletePkiSubscriber?.data as { subscriberName: string })?.subscriberName + ) + } + /> +
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx new file mode 100644 index 000000000..3ba473985 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx @@ -0,0 +1,188 @@ +import { + faBan, + faEllipsis, + faPencil, + faTrash, + faUserShield +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Badge, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; +import { useListWorkspacePkiSubscribers } from "@app/hooks/api"; +import { + getPkiSubscriberStatusBadgeVariant, + PkiSubscriberStatus, + pkiSubscriberStatusToNameMap +} from "@app/hooks/api/pkiSubscriber/constants"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deletePkiSubscriber", "pkiSubscriber", "pkiSubscriberStatus"]>, + data?: object + ) => void; +}; + +export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => { + const navigate = useNavigate(); + const { currentWorkspace } = useWorkspace(); + const { data, isPending } = useListWorkspacePkiSubscribers(currentWorkspace?.id || ""); + return ( +
+ + + + + + + + + + + {isPending && } + {!isPending && + data && + data.length > 0 && + data.map((subscriber) => { + return ( + + navigate({ + to: `/${ProjectType.CertificateManager}/$projectId/subscribers/$subscriberName` as const, + params: { + projectId: currentWorkspace.id, + subscriberName: subscriber.name + } + }) + } + > + + + + + + ); + })} + +
NameStatusCommon Name +
{subscriber.name} + + {pkiSubscriberStatusToNameMap[subscriber.status]} + + {subscriber.commonName} + + +
+ + + +
+
+ + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("pkiSubscriber", { + subscriberName: subscriber.name + }); + }} + disabled={!isAllowed} + icon={} + > + Edit Subscriber + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("pkiSubscriberStatus", { + subscriberName: subscriber.name, + status: + subscriber.status === PkiSubscriberStatus.ACTIVE + ? PkiSubscriberStatus.DISABLED + : PkiSubscriberStatus.ACTIVE + }); + }} + disabled={!isAllowed} + icon={} + > + {`${subscriber.status === PkiSubscriberStatus.ACTIVE ? "Disable" : "Enable"} Subscriber`} + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deletePkiSubscriber", { + subscriberName: subscriber.name + }); + }} + disabled={!isAllowed} + icon={} + > + Delete Subscriber + + )} + + +
+
+ {!isPending && data?.length === 0 && ( + + )} +
+
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/index.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/index.tsx new file mode 100644 index 000000000..4c9b89234 --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/index.tsx @@ -0,0 +1 @@ +export { PkiSubscriberSection } from "./PkiSubscriberSection"; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx new file mode 100644 index 000000000..d8d9fbadd --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { PkiSubscribersPage } from "./PkiSubscribersPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/" +)({ + component: PkiSubscribersPage +}); diff --git a/frontend/src/pages/cert-manager/layout.tsx b/frontend/src/pages/cert-manager/layout.tsx index 54a9b06d3..1b52793f8 100644 --- a/frontend/src/pages/cert-manager/layout.tsx +++ b/frontend/src/pages/cert-manager/layout.tsx @@ -31,7 +31,7 @@ export const Route = createFileRoute( { label: project.name, link: linkOptions({ - to: "/cert-manager/$projectId/overview", + to: "/cert-manager/$projectId/subscribers", params: { projectId: project.id } }) } diff --git a/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx b/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx index 4e33e5688..75b21b742 100644 --- a/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx +++ b/frontend/src/pages/organization/AdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx @@ -58,6 +58,15 @@ export const OrgAdminProjects = withPermission( await orgAdminAccessProject.mutateAsync({ projectId }); + if (type === ProjectType.CertificateManager) { + await navigate({ + to: "/cert-manager/$projectId/subscribers" as const, + params: { + projectId + } + }); + return; + } await navigate({ to: `/${type}/$projectId/overview` as const, params: { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/index.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/index.tsx deleted file mode 100644 index 537be9831..000000000 --- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { OrgAuthTab } from "./OrgAuthTab"; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx index 9cd59c0ad..bb2e9ea4d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx @@ -4,8 +4,8 @@ import { createNotification } from "@app/components/notifications"; import { Button, DeleteActionModal } from "@app/components/v2"; import { useOrganization, useOrgPermission } from "@app/context"; import { useDeleteOrgById } from "@app/hooks/api"; +import { clearSession } from "@app/hooks/api/users/queries"; import { usePopUp } from "@app/hooks/usePopUp"; -import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; export const OrgDeleteSection = () => { const navigate = useNavigate(); @@ -13,9 +13,7 @@ export const OrgDeleteSection = () => { const { membership } = useOrgPermission(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "deleteOrg" - ] as const); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteOrg"] as const); const { mutateAsync, isPending } = useDeleteOrgById(); @@ -32,9 +30,8 @@ export const OrgDeleteSection = () => { type: "success" }); - await navigateUserToOrg(navigate); - - handlePopUpClose("deleteOrg"); + clearSession(); + navigate({ to: "/login" }); } catch (err) { console.error(err); createNotification({ diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGenericAuthSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgGenericAuthSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGenericAuthSection.tsx rename to frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgGenericAuthSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx new file mode 100644 index 000000000..981681b7d --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx @@ -0,0 +1,35 @@ +import { Link } from "@tanstack/react-router"; + +import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { withPermission } from "@app/hoc"; + +import { OrgGenericAuthSection } from "./OrgGenericAuthSection"; +import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection"; + +export const OrgSecurityTab = withPermission( + () => { + return ( + <> + +

+ SSO Settings have been relocated:{" "} + + Click here to view SSO Settings + +

+
+ + + + ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Sso } +); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx similarity index 95% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx rename to frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx index 43e72bd41..bc020d3a1 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx @@ -86,13 +86,12 @@ export const OrgUserAccessTokenLimitSection = () => { ]; return ( -
+
-

User Token Expiration

+

Session Length

- This defines the maximum time a user token will be valid. After this time, the user will - need to re-authenticate. + Specify the duration of each login session for users in this organization.

{(isAllowed) => ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/index.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/index.tsx new file mode 100644 index 000000000..565772a72 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/index.tsx @@ -0,0 +1 @@ +export * from "./OrgSecurityTab"; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx index 92979d5f7..e5532838c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx @@ -10,9 +10,9 @@ import { ProjectType } from "@app/hooks/api/workspace/types"; import { AuditLogStreamsTab } from "../AuditLogStreamTab"; import { ImportTab } from "../ImportTab"; import { KmipTab } from "../KmipTab/OrgKmipTab"; -import { OrgAuthTab } from "../OrgAuthTab"; import { OrgEncryptionTab } from "../OrgEncryptionTab"; import { OrgGeneralTab } from "../OrgGeneralTab"; +import { OrgSecurityTab } from "../OrgSecurityTab"; import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab"; export const OrgTabGroup = () => { @@ -21,7 +21,7 @@ export const OrgTabGroup = () => { }); const tabs = [ { name: "General", key: "tab-org-general", component: OrgGeneralTab }, - { name: "Security", key: "tab-org-security", component: OrgAuthTab }, + { name: "Security", key: "tab-org-security", component: OrgSecurityTab }, { name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab }, { name: "Workflow Integrations", diff --git a/frontend/src/pages/organization/SsoPage/SsoPage.tsx b/frontend/src/pages/organization/SsoPage/SsoPage.tsx new file mode 100644 index 000000000..2ee19a56c --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/SsoPage.tsx @@ -0,0 +1,21 @@ +import { Helmet } from "react-helmet"; + +import { PageHeader } from "@app/components/v2"; + +import { SsoTabGroup } from "./components/SsoTabGroup"; + +export const SsoPage = () => { + return ( + <> + + Single Sign-On (SSO) + +
+
+ + +
+
+ + ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/ExternalGroupOrgRoleMappings.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/ExternalGroupOrgRoleMappings.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/ExternalGroupOrgRoleMappings.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/ExternalGroupOrgRoleMappings.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/GithubOrgSyncConfigModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/GithubOrgSyncConfigModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/GithubOrgSyncConfigModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/GithubOrgSyncConfigModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGithubSyncSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgGithubSyncSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGithubSyncSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgGithubSyncSection.tsx diff --git a/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgProvisioningTab.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgProvisioningTab.tsx new file mode 100644 index 000000000..43822aaff --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgProvisioningTab.tsx @@ -0,0 +1,17 @@ +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { withPermission } from "@app/hoc"; + +import { OrgGithubSyncSection } from "./OrgGithubSyncSection"; +import { OrgScimSection } from "./OrgSCIMSection"; + +export const OrgProvisioningTab = withPermission( + () => { + return ( + <> + + + + ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Sso } +); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgSCIMSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/OrgSCIMSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/ScimTokenModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/ScimTokenModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/ScimTokenModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/ScimTokenModal.tsx diff --git a/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/index.tsx b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/index.tsx new file mode 100644 index 000000000..c39b77b6a --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/components/OrgProvisioningTab/index.tsx @@ -0,0 +1 @@ +export * from "./OrgProvisioningTab"; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/LDAPGroupMapModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/LDAPGroupMapModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/LDAPGroupMapModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/LDAPGroupMapModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/LDAPModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/LDAPModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/LDAPModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/LDAPModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OIDCModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OIDCModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OIDCModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OIDCModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgLDAPSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgLDAPSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgOIDCSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgOIDCSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgOIDCSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgSSOSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSSOSection.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgSSOSection.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSSOSection.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSsoTab.tsx similarity index 93% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSsoTab.tsx index 05d105192..65f97cc2d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx +++ b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgSsoTab.tsx @@ -17,16 +17,12 @@ import { LoginMethod } from "@app/hooks/api/admin/types"; import { LDAPModal } from "./LDAPModal"; import { OIDCModal } from "./OIDCModal"; import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection"; -import { OrgGenericAuthSection } from "./OrgGenericAuthSection"; -import { OrgGithubSyncSection } from "./OrgGithubSyncSection"; import { OrgLDAPSection } from "./OrgLDAPSection"; import { OrgOIDCSection } from "./OrgOIDCSection"; -import { OrgScimSection } from "./OrgSCIMSection"; import { OrgSSOSection } from "./OrgSSOSection"; -import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection"; import { SSOModal } from "./SSOModal"; -export const OrgAuthTab = withPermission( +export const OrgSsoTab = withPermission( () => { const { config: { enabledLoginMethods } @@ -167,8 +163,6 @@ export const OrgAuthTab = withPermission( return ( <> - - {shouldShowCreateIdentityProviderView ? ( createIdentityProviderView ) : ( @@ -183,8 +177,6 @@ export const OrgAuthTab = withPermission( {isLdapConfigured && shouldDisplaySection(LoginMethod.LDAP) && } )} - - handlePopUpToggle("upgradePlan", isOpen)} diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/SSOModal.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/SSOModal.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/SSOModal.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/SSOModal.tsx diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/SSOModalHeader.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/SSOModalHeader.tsx similarity index 100% rename from frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/SSOModalHeader.tsx rename to frontend/src/pages/organization/SsoPage/components/OrgSsoTab/SSOModalHeader.tsx diff --git a/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/index.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/index.tsx new file mode 100644 index 000000000..fd02ae3e3 --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/index.tsx @@ -0,0 +1 @@ +export { OrgSsoTab } from "./OrgSsoTab"; diff --git a/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/SsoTabGroup.tsx b/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/SsoTabGroup.tsx new file mode 100644 index 000000000..273265285 --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/SsoTabGroup.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { useSearch } from "@tanstack/react-router"; + +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; + +import { OrgProvisioningTab } from "../OrgProvisioningTab"; +import { OrgSsoTab } from "../OrgSsoTab"; + +export const SsoTabGroup = () => { + const search = useSearch({ + from: ROUTE_PATHS.Organization.SsoPage.id + }); + const tabs = [ + { name: "General", key: "tab-sso-auth", component: OrgSsoTab }, + { name: "Provisioning", key: "tab-sso-identity", component: OrgProvisioningTab } + ]; + + const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); + + return ( + + + {tabs.map((tab) => ( + + {tab.name} + + ))} + + {tabs.map(({ key, component: Component }) => ( + + + + ))} + + ); +}; diff --git a/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/index.tsx b/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/index.tsx new file mode 100644 index 000000000..0aca705c8 --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/components/SsoTabGroup/index.tsx @@ -0,0 +1 @@ +export { SsoTabGroup } from "./SsoTabGroup"; diff --git a/frontend/src/pages/organization/SsoPage/route.tsx b/frontend/src/pages/organization/SsoPage/route.tsx new file mode 100644 index 000000000..c3b144573 --- /dev/null +++ b/frontend/src/pages/organization/SsoPage/route.tsx @@ -0,0 +1,33 @@ +import { faHome } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { SsoPage } from "./SsoPage"; + +const SettingsPageQueryParams = z.object({ + selectedTab: z.string().catch("") +}); + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/organization/sso" +)({ + component: SsoPage, + validateSearch: zodValidator(SettingsPageQueryParams), + search: { + middlewares: [stripSearchParams({ selectedTab: "" })] + }, + context: () => ({ + breadcrumbs: [ + { + label: "Home", + icon: () => , + link: linkOptions({ to: "/" }) + }, + { + label: "Single Sign-On (SSO)" + } + ] + }) +}); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSubscriberPermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSubscriberPermissionConditions.tsx new file mode 100644 index 000000000..58840fe2f --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSubscriberPermissionConditions.tsx @@ -0,0 +1,173 @@ +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + FormControl, + IconButton, + Input, + Select, + SelectItem, + Tooltip +} from "@app/components/v2"; +import { + PermissionConditionOperators, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext/types"; + +import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers"; +import { TFormSchema } from "./ProjectRoleModifySection.utils"; + +type Props = { + position?: number; + isDisabled?: boolean; +}; + +export const PkiSubscriberPermissionConditions = ({ position = 0, isDisabled }: Props) => { + const { + control, + watch, + formState: { errors } + } = useFormContext(); + + const permissionSubject = ProjectPermissionSub.PkiSubscribers; + const items = useFieldArray({ + control, + name: `permissions.${permissionSubject}.${position}.conditions` + }); + + return ( +
+

Conditions

+

+ Conditions determine when a policy will be applied (always if no conditions are present). +

+

+ All conditions must evaluate to true for the policy to take effect. +

+
+ {items.fields.map((el, index) => { + const condition = + (watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as { + lhs: string; + rhs: string; + operator: string; + }) || {}; + + return ( +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> + + + +
+
+ ( + + + + )} + /> +
+
+ items.remove(index)} + > + + +
+
+ ); + })} +
+ {errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && ( +
+ + {errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message} +
+ )} +
+ +
+
+ ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index bd5cdaa4e..57c456cf8 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -17,6 +17,7 @@ import { ProjectPermissionIdentityActions, ProjectPermissionKmipActions, ProjectPermissionMemberActions, + ProjectPermissionPkiSubscriberActions, ProjectPermissionSecretActions, ProjectPermissionSecretRotationActions, ProjectPermissionSecretSyncActions, @@ -131,6 +132,15 @@ const SshHostPolicyActionSchema = z.object({ [ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional() }); +const PkiSubscriberPolicyActionSchema = z.object({ + [ProjectPermissionPkiSubscriberActions.Read]: z.boolean().optional(), + [ProjectPermissionPkiSubscriberActions.Create]: z.boolean().optional(), + [ProjectPermissionPkiSubscriberActions.Edit]: z.boolean().optional(), + [ProjectPermissionPkiSubscriberActions.Delete]: z.boolean().optional(), + [ProjectPermissionPkiSubscriberActions.IssueCert]: z.boolean().optional(), + [ProjectPermissionPkiSubscriberActions.ListCerts]: z.boolean().optional() +}); + const SecretRollbackPolicyActionSchema = z.object({ read: z.boolean().optional(), create: z.boolean().optional() @@ -230,6 +240,12 @@ export const projectRoleFormSchema = z.object({ [ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]), + [ProjectPermissionSub.PkiSubscribers]: PkiSubscriberPolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), [ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]), @@ -271,6 +287,7 @@ type TConditionalFields = | ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretImports | ProjectPermissionSub.DynamicSecrets + | ProjectPermissionSub.PkiSubscribers | ProjectPermissionSub.SshHosts | ProjectPermissionSub.SecretRotation | ProjectPermissionSub.Identity; @@ -284,7 +301,8 @@ export const isConditionalSubjects = ( subject === ProjectPermissionSub.SecretFolders || subject === ProjectPermissionSub.Identity || subject === ProjectPermissionSub.SshHosts || - subject === ProjectPermissionSub.SecretRotation; + subject === ProjectPermissionSub.SecretRotation || + subject === ProjectPermissionSub.PkiSubscribers; const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const formConditions: z.infer = []; @@ -715,6 +733,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { inverted }); } + + if (subject === ProjectPermissionSub.PkiSubscribers) { + if (!formVal[subject]) formVal[subject] = []; + + formVal[subject]!.push({ + [ProjectPermissionPkiSubscriberActions.Edit]: action.includes( + ProjectPermissionPkiSubscriberActions.Edit + ), + [ProjectPermissionPkiSubscriberActions.Delete]: action.includes( + ProjectPermissionPkiSubscriberActions.Delete + ), + [ProjectPermissionPkiSubscriberActions.Create]: action.includes( + ProjectPermissionPkiSubscriberActions.Create + ), + [ProjectPermissionPkiSubscriberActions.Read]: action.includes( + ProjectPermissionPkiSubscriberActions.Read + ), + [ProjectPermissionPkiSubscriberActions.IssueCert]: action.includes( + ProjectPermissionPkiSubscriberActions.IssueCert + ), + [ProjectPermissionPkiSubscriberActions.ListCerts]: action.includes( + ProjectPermissionPkiSubscriberActions.ListCerts + ), + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted + }); + } }); return formVal; @@ -1104,6 +1149,17 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Remove", value: "delete" } ] }, + [ProjectPermissionSub.PkiSubscribers]: { + title: "PKI Subscribers", + actions: [ + { label: "Read", value: ProjectPermissionPkiSubscriberActions.Read }, + { label: "Create", value: ProjectPermissionPkiSubscriberActions.Create }, + { label: "Modify", value: ProjectPermissionPkiSubscriberActions.Edit }, + { label: "Remove", value: ProjectPermissionPkiSubscriberActions.Delete }, + { label: "Issue Certificate", value: ProjectPermissionPkiSubscriberActions.IssueCert }, + { label: "List Certificates", value: ProjectPermissionPkiSubscriberActions.ListCerts } + ] + }, [ProjectPermissionSub.PkiCollections]: { title: "PKI Collections", actions: [ @@ -1233,6 +1289,7 @@ const KmsPermissionSubjects = (enabled = false) => ({ const CertificateManagerPermissionSubjects = (enabled = false) => ({ [ProjectPermissionSub.PkiCollections]: enabled, [ProjectPermissionSub.PkiAlerts]: enabled, + [ProjectPermissionSub.PkiSubscribers]: enabled, [ProjectPermissionSub.CertificateAuthorities]: enabled, [ProjectPermissionSub.CertificateTemplates]: enabled, [ProjectPermissionSub.Certificates]: enabled diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index 8dc9a2fa1..547ee638a 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -23,6 +23,7 @@ import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies"; import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions"; import { PermissionEmptyState } from "./PermissionEmptyState"; +import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions"; import { formRolePermission2API, isConditionalSubjects, @@ -59,6 +60,10 @@ export const renderConditionalComponents = ( return ; } + if (subject === ProjectPermissionSub.PkiSubscribers) { + return ; + } + return ; } diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx index e3c53860e..62f563cce 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostsTable.tsx @@ -30,7 +30,7 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { ProjectPermissionSshHostActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { fetchSshHostUserCaPublicKey, useListWorkspaceSshHosts } from "@app/hooks/api"; import { LoginMappingSource } from "@app/hooks/api/sshHost/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -221,7 +221,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => { Download User CA Public Key {(isAllowed) => ( @@ -243,7 +243,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => { )} {(isAllowed) => ( diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 5ca262a1d..f457cf209 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -42,6 +42,7 @@ import { Route as adminLayoutImport } from './pages/admin/layout' import { Route as authProviderSuccessPageRouteImport } from './pages/auth/ProviderSuccessPage/route' import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route' import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route' +import { Route as organizationSsoPageRouteImport } from './pages/organization/SsoPage/route' import { Route as organizationSecretScanningPageRouteImport } from './pages/organization/SecretScanningPage/route' import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route' import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route' @@ -118,8 +119,10 @@ import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/se import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route' import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route' import { Route as organizationAppConnectionsOauthCallbackPageRouteImport } from './pages/organization/AppConnections/OauthCallbackPage/route' +import { Route as certManagerPkiSubscriberDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSubscriberDetailsByIDPage/route' import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route' import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route' +import { Route as certManagerPkiSubscribersPageRouteImport } from './pages/cert-manager/PkiSubscribersPage/route' import { Route as secretManagerIntegrationsWindmillConfigurePageRouteImport } from './pages/secret-manager/integrations/WindmillConfigurePage/route' import { Route as secretManagerIntegrationsWindmillAuthorizePageRouteImport } from './pages/secret-manager/integrations/WindmillAuthorizePage/route' import { Route as secretManagerIntegrationsVercelConfigurePageRouteImport } from './pages/secret-manager/integrations/VercelConfigurePage/route' @@ -250,6 +253,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLa createFileRoute( '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations', )() +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport = + createFileRoute( + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers', + )() // Create/Update Routes @@ -539,6 +546,12 @@ const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRoute = getParentRoute: () => organizationLayoutRoute, } as any) +const organizationSsoPageRouteRoute = organizationSsoPageRouteImport.update({ + id: '/sso', + path: '/sso', + getParentRoute: () => AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, +} as any) + const organizationSecretScanningPageRouteRoute = organizationSecretScanningPageRouteImport.update({ id: '/secret-scanning', @@ -848,6 +861,15 @@ const secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute = } as any, ) +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute = + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport.update( + { + id: '/subscribers', + path: '/subscribers', + getParentRoute: () => certManagerLayoutRoute, + } as any, + ) + const projectAccessControlPageRouteCertManagerRoute = projectAccessControlPageRouteCertManagerImport.update({ id: '/access-management', @@ -949,8 +971,8 @@ const certManagerSettingsPageRouteRoute = const certManagerCertificatesPageRouteRoute = certManagerCertificatesPageRouteImport.update({ - id: '/overview', - path: '/overview', + id: '/certificates', + path: '/certificates', getParentRoute: () => certManagerLayoutRoute, } as any) @@ -1103,6 +1125,14 @@ const organizationAppConnectionsOauthCallbackPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute, } as any) +const certManagerPkiSubscriberDetailsByIDPageRouteRoute = + certManagerPkiSubscriberDetailsByIDPageRouteImport.update({ + id: '/$subscriberName', + path: '/$subscriberName', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute, + } as any) + const certManagerCertAuthDetailsByIDPageRouteRoute = certManagerCertAuthDetailsByIDPageRouteImport.update({ id: '/ca/$caId', @@ -1118,6 +1148,14 @@ const secretManagerIntegrationsListPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute, } as any) +const certManagerPkiSubscribersPageRouteRoute = + certManagerPkiSubscribersPageRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute, + } as any) + const secretManagerIntegrationsWindmillConfigurePageRouteRoute = secretManagerIntegrationsWindmillConfigurePageRouteImport.update({ id: '/windmill/create', @@ -2017,6 +2055,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationSecretScanningPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } + '/_authenticate/_inject-org-details/_org-layout/organization/sso': { + id: '/_authenticate/_inject-org-details/_org-layout/organization/sso' + path: '/sso' + fullPath: '/organization/sso' + preLoaderRoute: typeof organizationSsoPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport + } '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': { id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId' path: '/cert-manager/$projectId' @@ -2234,10 +2279,10 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof certManagerCertificateAuthoritiesPageRouteImport parentRoute: typeof certManagerLayoutImport } - '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview': { - id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview' - path: '/overview' - fullPath: '/cert-manager/$projectId/overview' + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates' + path: '/certificates' + fullPath: '/cert-manager/$projectId/certificates' preLoaderRoute: typeof certManagerCertificatesPageRouteImport parentRoute: typeof certManagerLayoutImport } @@ -2346,6 +2391,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers' + path: '/subscribers' + fullPath: '/cert-manager/$projectId/subscribers' + preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport + parentRoute: typeof certManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback': { id: '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback' path: '/azure-app-configuration/oauth2/callback' @@ -2437,6 +2489,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectAccessControlPageRouteSshImport parentRoute: typeof sshLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/' + path: '/' + fullPath: '/cert-manager/$projectId/subscribers/' + preLoaderRoute: typeof certManagerPkiSubscribersPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport + } '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/': { id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/' path: '/' @@ -2451,6 +2510,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName' + path: '/$subscriberName' + fullPath: '/cert-manager/$projectId/subscribers/$subscriberName' + preLoaderRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteImport + parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport + } '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': { id: '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback' path: '/$appConnection/oauth/callback' @@ -3227,6 +3293,7 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren { organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute organizationSecretScanningPageRouteRoute: typeof organizationSecretScanningPageRouteRoute + organizationSsoPageRouteRoute: typeof organizationSsoPageRouteRoute AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren @@ -3254,6 +3321,7 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica organizationBillingPageRouteRoute: organizationBillingPageRouteRoute, organizationSecretScanningPageRouteRoute: organizationSecretScanningPageRouteRoute, + organizationSsoPageRouteRoute: organizationSsoPageRouteRoute, AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: @@ -3292,12 +3360,31 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren, ) +interface AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren { + certManagerPkiSubscribersPageRouteRoute: typeof certManagerPkiSubscribersPageRouteRoute + certManagerPkiSubscriberDetailsByIDPageRouteRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute +} + +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren = + { + certManagerPkiSubscribersPageRouteRoute: + certManagerPkiSubscribersPageRouteRoute, + certManagerPkiSubscriberDetailsByIDPageRouteRoute: + certManagerPkiSubscriberDetailsByIDPageRouteRoute, + } + +const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren = + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute._addFileChildren( + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren, + ) + interface certManagerLayoutRouteChildren { certManagerAlertingPageRouteRoute: typeof certManagerAlertingPageRouteRoute certManagerCertificateAuthoritiesPageRouteRoute: typeof certManagerCertificateAuthoritiesPageRouteRoute certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute projectIdentityDetailsByIDPageRouteCertManagerRoute: typeof projectIdentityDetailsByIDPageRouteCertManagerRoute projectMemberDetailsByIDPageRouteCertManagerRoute: typeof projectMemberDetailsByIDPageRouteCertManagerRoute @@ -3313,6 +3400,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = { certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute, projectAccessControlPageRouteCertManagerRoute: projectAccessControlPageRouteCertManagerRoute, + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: + AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren, certManagerCertAuthDetailsByIDPageRouteRoute: certManagerCertAuthDetailsByIDPageRouteRoute, projectIdentityDetailsByIDPageRouteCertManagerRoute: @@ -3956,6 +4045,7 @@ export interface FileRoutesByFullPath { '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute '/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute + '/organization/sso': typeof organizationSsoPageRouteRoute '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren '/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren @@ -3983,7 +4073,7 @@ export interface FileRoutesByFullPath { '/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute '/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute '/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute - '/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute + '/cert-manager/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute '/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute '/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute '/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute @@ -3999,6 +4089,7 @@ export interface FileRoutesByFullPath { '/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute '/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/cert-manager/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren '/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute '/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsRouteBitbucketOauthRedirectRoute @@ -4012,8 +4103,10 @@ export interface FileRoutesByFullPath { '/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute '/secret-manager/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute + '/cert-manager/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/cert-manager/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute @@ -4143,6 +4236,7 @@ export interface FileRoutesByTo { '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute '/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute + '/organization/sso': typeof organizationSsoPageRouteRoute '/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren '/kms/$projectId': typeof kmsLayoutRouteWithChildren '/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren @@ -4166,7 +4260,7 @@ export interface FileRoutesByTo { '/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute '/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute '/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute - '/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute + '/cert-manager/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute '/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute '/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute '/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute @@ -4194,8 +4288,10 @@ export interface FileRoutesByTo { '/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute '/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute + '/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute '/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/cert-manager/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute @@ -4335,6 +4431,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organization/sso': typeof organizationSsoPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren @@ -4366,7 +4463,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout': typeof sshLayoutRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting': typeof certManagerAlertingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview': typeof certManagerCertificatesPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates': typeof certManagerCertificatesPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings': typeof certManagerSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute @@ -4382,6 +4479,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/_authenticate/_inject-org-details/_org-layout/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute '/_authenticate/_inject-org-details/_org-layout/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsRouteBitbucketOauthRedirectRoute @@ -4395,8 +4493,10 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management': typeof projectAccessControlPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute @@ -4532,6 +4632,7 @@ export interface FileRouteTypes { | '/organization/audit-logs' | '/organization/billing' | '/organization/secret-scanning' + | '/organization/sso' | '/cert-manager/$projectId' | '/kms/$projectId' | '/organization/app-connections' @@ -4559,7 +4660,7 @@ export interface FileRouteTypes { | '/organization/ssh/settings' | '/cert-manager/$projectId/alerting' | '/cert-manager/$projectId/certificate-authorities' - | '/cert-manager/$projectId/overview' + | '/cert-manager/$projectId/certificates' | '/cert-manager/$projectId/settings' | '/kms/$projectId/kmip' | '/kms/$projectId/overview' @@ -4575,6 +4676,7 @@ export interface FileRouteTypes { | '/ssh/$projectId/overview' | '/ssh/$projectId/settings' | '/cert-manager/$projectId/access-management' + | '/cert-manager/$projectId/subscribers' | '/integrations/azure-app-configuration/oauth2/callback' | '/integrations/azure-key-vault/oauth2/callback' | '/integrations/bitbucket/oauth2/callback' @@ -4588,8 +4690,10 @@ export interface FileRouteTypes { | '/secret-manager/$projectId/access-management' | '/secret-manager/$projectId/integrations' | '/ssh/$projectId/access-management' + | '/cert-manager/$projectId/subscribers/' | '/secret-manager/$projectId/integrations/' | '/cert-manager/$projectId/ca/$caId' + | '/cert-manager/$projectId/subscribers/$subscriberName' | '/organization/app-connections/$appConnection/oauth/callback' | '/secret-manager/$projectId/integrations/$integrationId' | '/secret-manager/$projectId/integrations/select-integration-auth' @@ -4718,6 +4822,7 @@ export interface FileRouteTypes { | '/organization/audit-logs' | '/organization/billing' | '/organization/secret-scanning' + | '/organization/sso' | '/cert-manager/$projectId' | '/kms/$projectId' | '/secret-manager/$projectId' @@ -4741,7 +4846,7 @@ export interface FileRouteTypes { | '/organization/ssh/settings' | '/cert-manager/$projectId/alerting' | '/cert-manager/$projectId/certificate-authorities' - | '/cert-manager/$projectId/overview' + | '/cert-manager/$projectId/certificates' | '/cert-manager/$projectId/settings' | '/kms/$projectId/kmip' | '/kms/$projectId/overview' @@ -4769,8 +4874,10 @@ export interface FileRouteTypes { | '/kms/$projectId/access-management' | '/secret-manager/$projectId/access-management' | '/ssh/$projectId/access-management' + | '/cert-manager/$projectId/subscribers' | '/secret-manager/$projectId/integrations' | '/cert-manager/$projectId/ca/$caId' + | '/cert-manager/$projectId/subscribers/$subscriberName' | '/organization/app-connections/$appConnection/oauth/callback' | '/secret-manager/$projectId/integrations/$integrationId' | '/secret-manager/$projectId/integrations/select-integration-auth' @@ -4908,6 +5015,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/organization/billing' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning' + | '/_authenticate/_inject-org-details/_org-layout/organization/sso' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections' @@ -4939,7 +5047,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities' - | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview' @@ -4955,6 +5063,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers' | '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback' | '/_authenticate/_inject-org-details/_org-layout/integrations/azure-key-vault/oauth2/callback' | '/_authenticate/_inject-org-details/_org-layout/integrations/bitbucket/oauth2/callback' @@ -4968,8 +5077,10 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth' @@ -5304,6 +5415,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/audit-logs", "/_authenticate/_inject-org-details/_org-layout/organization/billing", "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning", + "/_authenticate/_inject-org-details/_org-layout/organization/sso", "/_authenticate/_inject-org-details/_org-layout/organization/app-connections", "/_authenticate/_inject-org-details/_org-layout/organization/gateways", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing", @@ -5353,6 +5465,10 @@ export const routeTree = rootRoute "filePath": "organization/SecretScanningPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, + "/_authenticate/_inject-org-details/_org-layout/organization/sso": { + "filePath": "organization/SsoPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/organization" + }, "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout", @@ -5486,9 +5602,10 @@ export const routeTree = rootRoute "children": [ "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities", - "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId", @@ -5550,7 +5667,7 @@ export const routeTree = rootRoute "filePath": "cert-manager/CertificateAuthoritiesPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, - "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview": { + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates": { "filePath": "cert-manager/CertificatesPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, @@ -5614,6 +5731,14 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-cert-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers": { + "filePath": "", + "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout", + "children": [ + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName" + ] + }, "/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback": { "filePath": "secret-manager/integrations/route-azure-app-configurations-oauth-redirect.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/integrations" @@ -5746,6 +5871,10 @@ export const routeTree = rootRoute "filePath": "project/AccessControlPage/route-ssh.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" }, + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/": { + "filePath": "cert-manager/PkiSubscribersPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers" + }, "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/": { "filePath": "secret-manager/IntegrationsListPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations" @@ -5754,6 +5883,10 @@ export const routeTree = rootRoute "filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName": { + "filePath": "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers" + }, "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback": { "filePath": "organization/AppConnections/OauthCallbackPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index d1beb5507..d6553e663 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -28,6 +28,7 @@ const organizationRoutes = route("/organization", [ index("organization/SettingsPage/route.tsx"), route("/oauth/callback", "organization/SettingsPage/OauthCallbackPage/route.tsx") ]), + route("/sso", "organization/SsoPage/route.tsx"), route("/secret-scanning", "organization/SecretScanningPage/route.tsx"), route("/groups/$groupId", "organization/GroupDetailsByIDPage/route.tsx"), route("/members/$membershipId", "organization/UserDetailsByIDPage/route.tsx"), @@ -288,7 +289,11 @@ const secretManagerIntegrationsRedirect = route("/integrations", [ const certManagerRoutes = route("/cert-manager/$projectId", [ layout("cert-manager-layout", "cert-manager/layout.tsx", [ - route("/overview", "cert-manager/CertificatesPage/route.tsx"), + route("/subscribers", [ + index("cert-manager/PkiSubscribersPage/route.tsx"), + route("/$subscriberName", "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx") + ]), + route("/certificates", "cert-manager/CertificatesPage/route.tsx"), route("/certificate-authorities", "cert-manager/CertificateAuthoritiesPage/route.tsx"), route("/alerting", "cert-manager/AlertingPage/route.tsx"), route("/ca/$caId", "cert-manager/CertAuthDetailsByIDPage/route.tsx"),