mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 16:28:40 +00:00
Merge pull request #3569 from Infisical/pki-subscriber
Infisical PKI: Subscriber Functionality
This commit is contained in:
@@ -28,3 +28,15 @@ frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow
|
|||||||
docs/cli/commands/user.mdx:generic-api-key:51
|
docs/cli/commands/user.mdx:generic-api-key:51
|
||||||
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76
|
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
|
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
|
||||||
|
|||||||
Vendored
+2
@@ -80,6 +80,7 @@ import { TOrgServiceFactory } from "@app/services/org/org-service";
|
|||||||
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||||
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||||
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-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 { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||||
@@ -232,6 +233,7 @@ declare module "fastify" {
|
|||||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
certificateEst: TCertificateEstServiceFactory;
|
certificateEst: TCertificateEstServiceFactory;
|
||||||
pkiCollection: TPkiCollectionServiceFactory;
|
pkiCollection: TPkiCollectionServiceFactory;
|
||||||
|
pkiSubscriber: TPkiSubscriberServiceFactory;
|
||||||
secretScanning: TSecretScanningServiceFactory;
|
secretScanning: TSecretScanningServiceFactory;
|
||||||
license: TLicenseServiceFactory;
|
license: TLicenseServiceFactory;
|
||||||
trustedIp: TTrustedIpServiceFactory;
|
trustedIp: TTrustedIpServiceFactory;
|
||||||
|
|||||||
Vendored
+8
@@ -209,6 +209,9 @@ import {
|
|||||||
TPkiCollections,
|
TPkiCollections,
|
||||||
TPkiCollectionsInsert,
|
TPkiCollectionsInsert,
|
||||||
TPkiCollectionsUpdate,
|
TPkiCollectionsUpdate,
|
||||||
|
TPkiSubscribers,
|
||||||
|
TPkiSubscribersInsert,
|
||||||
|
TPkiSubscribersUpdate,
|
||||||
TProjectBots,
|
TProjectBots,
|
||||||
TProjectBotsInsert,
|
TProjectBotsInsert,
|
||||||
TProjectBotsUpdate,
|
TProjectBotsUpdate,
|
||||||
@@ -564,6 +567,11 @@ declare module "knex/types/tables" {
|
|||||||
TPkiCollectionItemsInsert,
|
TPkiCollectionItemsInsert,
|
||||||
TPkiCollectionItemsUpdate
|
TPkiCollectionItemsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.PkiSubscriber]: KnexOriginal.CompositeTableType<
|
||||||
|
TPkiSubscribers,
|
||||||
|
TPkiSubscribersInsert,
|
||||||
|
TPkiSubscribersUpdate
|
||||||
|
>;
|
||||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
TUserGroupMembershipInsert,
|
TUserGroupMembershipInsert,
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -24,7 +24,8 @@ export const CertificatesSchema = z.object({
|
|||||||
caCertId: z.string().uuid(),
|
caCertId: z.string().uuid(),
|
||||||
certificateTemplateId: z.string().uuid().nullable().optional(),
|
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||||
keyUsages: z.string().array().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<typeof CertificatesSchema>;
|
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export * from "./organizations";
|
|||||||
export * from "./pki-alerts";
|
export * from "./pki-alerts";
|
||||||
export * from "./pki-collection-items";
|
export * from "./pki-collection-items";
|
||||||
export * from "./pki-collections";
|
export * from "./pki-collections";
|
||||||
|
export * from "./pki-subscribers";
|
||||||
export * from "./project-bots";
|
export * from "./project-bots";
|
||||||
export * from "./project-environments";
|
export * from "./project-environments";
|
||||||
export * from "./project-gateways";
|
export * from "./project-gateways";
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export enum TableName {
|
|||||||
CertificateBody = "certificate_bodies",
|
CertificateBody = "certificate_bodies",
|
||||||
CertificateSecret = "certificate_secrets",
|
CertificateSecret = "certificate_secrets",
|
||||||
CertificateTemplate = "certificate_templates",
|
CertificateTemplate = "certificate_templates",
|
||||||
|
PkiSubscriber = "pki_subscribers",
|
||||||
PkiAlert = "pki_alerts",
|
PkiAlert = "pki_alerts",
|
||||||
PkiCollection = "pki_collections",
|
PkiCollection = "pki_collections",
|
||||||
PkiCollectionItem = "pki_collection_items",
|
PkiCollectionItem = "pki_collection_items",
|
||||||
|
|||||||
@@ -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<typeof PkiSubscribersSchema>;
|
||||||
|
export type TPkiSubscribersInsert = Omit<z.input<typeof PkiSubscribersSchema>, TImmutableDBKeys>;
|
||||||
|
export type TPkiSubscribersUpdate = Partial<Omit<z.input<typeof PkiSubscribersSchema>, TImmutableDBKeys>>;
|
||||||
@@ -73,7 +73,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const host = await server.services.sshHost.getSshHost({
|
const host = await server.services.sshHost.getSshHostById({
|
||||||
sshHostId: req.params.sshHostId,
|
sshHostId: req.params.sshHostId,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { TProjectPermission } from "@app/lib/types";
|
|||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
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 { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||||
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-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",
|
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
|
||||||
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
||||||
DELETE_PKI_COLLECTION_ITEM = "delete-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",
|
CREATE_KMS = "create-kms",
|
||||||
UPDATE_KMS = "update-kms",
|
UPDATE_KMS = "update-kms",
|
||||||
DELETE_KMS = "delete-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 {
|
interface CreateKmsEvent {
|
||||||
type: EventType.CREATE_KMS;
|
type: EventType.CREATE_KMS;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2928,6 +3006,13 @@ export type Event =
|
|||||||
| GetPkiCollectionItems
|
| GetPkiCollectionItems
|
||||||
| AddPkiCollectionItem
|
| AddPkiCollectionItem
|
||||||
| DeletePkiCollectionItem
|
| DeletePkiCollectionItem
|
||||||
|
| CreatePkiSubscriber
|
||||||
|
| UpdatePkiSubscriber
|
||||||
|
| DeletePkiSubscriber
|
||||||
|
| GetPkiSubscriber
|
||||||
|
| IssuePkiSubscriberCert
|
||||||
|
| SignPkiSubscriberCert
|
||||||
|
| ListPkiSubscriberCerts
|
||||||
| CreateKmsEvent
|
| CreateKmsEvent
|
||||||
| UpdateKmsEvent
|
| UpdateKmsEvent
|
||||||
| DeleteKmsEvent
|
| DeleteKmsEvent
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ProjectPermissionIdentityActions,
|
ProjectPermissionIdentityActions,
|
||||||
ProjectPermissionKmipActions,
|
ProjectPermissionKmipActions,
|
||||||
ProjectPermissionMemberActions,
|
ProjectPermissionMemberActions,
|
||||||
|
ProjectPermissionPkiSubscriberActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSecretRotationActions,
|
ProjectPermissionSecretRotationActions,
|
||||||
ProjectPermissionSecretSyncActions,
|
ProjectPermissionSecretSyncActions,
|
||||||
@@ -76,6 +77,18 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.SshHosts
|
ProjectPermissionSub.SshHosts
|
||||||
);
|
);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionPkiSubscriberActions.Edit,
|
||||||
|
ProjectPermissionPkiSubscriberActions.Read,
|
||||||
|
ProjectPermissionPkiSubscriberActions.Create,
|
||||||
|
ProjectPermissionPkiSubscriberActions.Delete,
|
||||||
|
ProjectPermissionPkiSubscriberActions.IssueCert,
|
||||||
|
ProjectPermissionPkiSubscriberActions.ListCerts
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.PkiSubscribers
|
||||||
|
);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionMemberActions.Create,
|
ProjectPermissionMemberActions.Create,
|
||||||
@@ -338,6 +351,7 @@ const buildMemberPermissionRules = () => {
|
|||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
|
||||||
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
|
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
|
||||||
|
can([ProjectPermissionPkiSubscriberActions.Read], ProjectPermissionSub.PkiSubscribers);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ export enum ProjectPermissionSshHostActions {
|
|||||||
IssueHostCert = "issue-host-cert"
|
IssueHostCert = "issue-host-cert"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionPkiSubscriberActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
IssueCert = "issue-cert",
|
||||||
|
ListCerts = "list-certs"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretSyncActions {
|
export enum ProjectPermissionSecretSyncActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@@ -143,6 +152,7 @@ export enum ProjectPermissionSub {
|
|||||||
SshCertificateTemplates = "ssh-certificate-templates",
|
SshCertificateTemplates = "ssh-certificate-templates",
|
||||||
SshHosts = "ssh-hosts",
|
SshHosts = "ssh-hosts",
|
||||||
SshHostGroups = "ssh-host-groups",
|
SshHostGroups = "ssh-host-groups",
|
||||||
|
PkiSubscribers = "pki-subscribers",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
@@ -190,6 +200,11 @@ export type SshHostSubjectFields = {
|
|||||||
hostname: string;
|
hostname: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PkiSubscriberSubjectFields = {
|
||||||
|
name: string;
|
||||||
|
// (dangtony98): consider adding [commonName] as a subject field in the future
|
||||||
|
};
|
||||||
|
|
||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
@@ -249,6 +264,13 @@ export type ProjectPermissionSet =
|
|||||||
ProjectPermissionSshHostActions,
|
ProjectPermissionSshHostActions,
|
||||||
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
||||||
]
|
]
|
||||||
|
| [
|
||||||
|
ProjectPermissionPkiSubscriberActions,
|
||||||
|
(
|
||||||
|
| ProjectPermissionSub.PkiSubscribers
|
||||||
|
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
|
||||||
|
)
|
||||||
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
@@ -399,6 +421,21 @@ const SshHostConditionSchema = z
|
|||||||
})
|
})
|
||||||
.partial();
|
.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 = [
|
const GeneralPermissionSchema = [
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
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."
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
).optional()
|
).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({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||||
|
|||||||
@@ -186,6 +186,33 @@ export const sshHostGroupServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
|
const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (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(
|
await sshHostGroupDAL.updateById(
|
||||||
sshHostGroupId,
|
sshHostGroupId,
|
||||||
{
|
{
|
||||||
@@ -193,6 +220,8 @@ export const sshHostGroupServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loginMappings) {
|
if (loginMappings) {
|
||||||
await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx);
|
await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx);
|
||||||
if (loginMappings.length) {
|
if (loginMappings.length) {
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ export const sshHostServiceFactory = ({
|
|||||||
return host;
|
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);
|
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||||
if (!host) {
|
if (!host) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -631,7 +631,7 @@ export const sshHostServiceFactory = ({
|
|||||||
createSshHost,
|
createSshHost,
|
||||||
updateSshHost,
|
updateSshHost,
|
||||||
deleteSshHost,
|
deleteSshHost,
|
||||||
getSshHost,
|
getSshHostById,
|
||||||
issueSshHostUserCert,
|
issueSshHostUserCert,
|
||||||
issueSshHostHostCert,
|
issueSshHostHostCert,
|
||||||
getSshHostUserCaPk,
|
getSshHostUserCaPk,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export enum ApiDocsTags {
|
|||||||
PkiCertificateTemplates = "PKI Certificate Templates",
|
PkiCertificateTemplates = "PKI Certificate Templates",
|
||||||
PkiCertificateCollections = "PKI Certificate Collections",
|
PkiCertificateCollections = "PKI Certificate Collections",
|
||||||
PkiAlerting = "PKI Alerting",
|
PkiAlerting = "PKI Alerting",
|
||||||
|
PkiSubscribers = "PKI Subscribers",
|
||||||
SshCertificates = "SSH Certificates",
|
SshCertificates = "SSH Certificates",
|
||||||
SshCertificateAuthorities = "SSH Certificate Authorities",
|
SshCertificateAuthorities = "SSH Certificate Authorities",
|
||||||
SshCertificateTemplates = "SSH Certificate Templates",
|
SshCertificateTemplates = "SSH Certificate Templates",
|
||||||
@@ -639,6 +640,9 @@ export const PROJECTS = {
|
|||||||
commonName: "The common name of the certificate to filter by.",
|
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.",
|
offset: "The offset to start from. If you enter 10, it will start from the 10th certificate.",
|
||||||
limit: "The number of certificates to return."
|
limit: "The number of certificates to return."
|
||||||
|
},
|
||||||
|
LIST_PKI_SUBSCRIBERS: {
|
||||||
|
projectId: "The ID of the project to list PKI subscribers for."
|
||||||
}
|
}
|
||||||
} as const;
|
} 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 = {
|
export const PKI_COLLECTIONS = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectId: "The ID of the project to create the PKI collection in.",
|
projectId: "The ID of the project to create the PKI collection in.",
|
||||||
|
|||||||
@@ -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 { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||||
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||||
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
|
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 { projectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||||
@@ -828,6 +830,7 @@ export const registerRoutes = async (
|
|||||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||||
const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db);
|
const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db);
|
||||||
|
const pkiSubscriberDAL = pkiSubscriberDALFactory(db);
|
||||||
|
|
||||||
const certificateService = certificateServiceFactory({
|
const certificateService = certificateServiceFactory({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
@@ -962,6 +965,20 @@ export const registerRoutes = async (
|
|||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pkiSubscriberService = pkiSubscriberServiceFactory({
|
||||||
|
pkiSubscriberDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
const projectTemplateService = projectTemplateServiceFactory({
|
const projectTemplateService = projectTemplateServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -1059,6 +1076,7 @@ export const registerRoutes = async (
|
|||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
|
pkiSubscriberDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
@@ -1745,6 +1763,7 @@ export const registerRoutes = async (
|
|||||||
certificateEst: certificateEstService,
|
certificateEst: certificateEstService,
|
||||||
pkiAlert: pkiAlertService,
|
pkiAlert: pkiAlertService,
|
||||||
pkiCollection: pkiCollectionService,
|
pkiCollection: pkiCollectionService,
|
||||||
|
pkiSubscriber: pkiSubscriberService,
|
||||||
secretScanning: secretScanningService,
|
secretScanning: secretScanningService,
|
||||||
license: licenseService,
|
license: licenseService,
|
||||||
trustedIp: trustedIpService,
|
trustedIp: trustedIpService,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { registerOrgRouter } from "./organization-router";
|
|||||||
import { registerPasswordRouter } from "./password-router";
|
import { registerPasswordRouter } from "./password-router";
|
||||||
import { registerPkiAlertRouter } from "./pki-alert-router";
|
import { registerPkiAlertRouter } from "./pki-alert-router";
|
||||||
import { registerPkiCollectionRouter } from "./pki-collection-router";
|
import { registerPkiCollectionRouter } from "./pki-collection-router";
|
||||||
|
import { registerPkiSubscriberRouter } from "./pki-subscriber-router";
|
||||||
import { registerProjectEnvRouter } from "./project-env-router";
|
import { registerProjectEnvRouter } from "./project-env-router";
|
||||||
import { registerProjectKeyRouter } from "./project-key-router";
|
import { registerProjectKeyRouter } from "./project-key-router";
|
||||||
import { registerProjectMembershipRouter } from "./project-membership-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(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||||
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
||||||
|
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
|
||||||
},
|
},
|
||||||
{ prefix: "/pki" }
|
{ prefix: "/pki" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -24,6 +24,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
|||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
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 { ProjectFilterType } from "@app/services/project/project-types";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-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({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:projectId/certificate-templates",
|
url: "/:projectId/certificate-templates",
|
||||||
@@ -628,6 +661,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHosts],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId)
|
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId)
|
||||||
}),
|
}),
|
||||||
@@ -666,6 +701,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.SshHostGroups],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOST_GROUPS.projectId)
|
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOST_GROUPS.projectId)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1169,7 +1169,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
ProjectPermissionSub.Certificates
|
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.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||||
if (ca.requireTemplateForIssuance && !certificateTemplate) {
|
if (ca.requireTemplateForIssuance && !certificateTemplate) {
|
||||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
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.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||||
if (ca.requireTemplateForIssuance && !certificateTemplate) {
|
if (ca.requireTemplateForIssuance && !certificateTemplate) {
|
||||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ const isValidDate = (dateString: string) => {
|
|||||||
|
|
||||||
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
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
|
export const validateAltNamesField = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
@@ -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 {
|
return {
|
||||||
...certificateOrm,
|
...certificateOrm,
|
||||||
countCertificatesInProject
|
countCertificatesInProject,
|
||||||
|
countCertificatesForPkiSubscriber
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<typeof pkiSubscriberDALFactory>;
|
||||||
|
|
||||||
|
export const pkiSubscriberDALFactory = (db: TDbClient) => {
|
||||||
|
const pkiSubscriberOrm = ormify(db, TableName.PkiSubscriber);
|
||||||
|
return pkiSubscriberOrm;
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
});
|
||||||
@@ -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<TCertificateAuthorityDALFactory, "findById">;
|
||||||
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||||
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "countCertificatesForPkiSubscriber" | "find">;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPkiSubscriberServiceFactory = ReturnType<typeof pkiSubscriberServiceFactory>;
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionPkiSubscriberActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSshHostActions,
|
ProjectPermissionSshHostActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
@@ -35,6 +36,7 @@ import { groupBy } from "@app/lib/fn";
|
|||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { TQueueServiceFactory } from "@app/queue";
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
@@ -86,6 +88,7 @@ import {
|
|||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertificateTemplatesDTO,
|
TListProjectCertificateTemplatesDTO,
|
||||||
TListProjectCertsDTO,
|
TListProjectCertsDTO,
|
||||||
|
TListProjectPkiSubscribersDTO,
|
||||||
TListProjectsDTO,
|
TListProjectsDTO,
|
||||||
TListProjectSshCasDTO,
|
TListProjectSshCasDTO,
|
||||||
TListProjectSshCertificatesDTO,
|
TListProjectSshCertificatesDTO,
|
||||||
@@ -145,6 +148,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
"findById" | "findByIdWithWorkflowIntegrationDetails"
|
"findById" | "findByIdWithWorkflowIntegrationDetails"
|
||||||
>;
|
>;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
|
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||||
@@ -207,6 +211,7 @@ export const projectServiceFactory = ({
|
|||||||
certificateTemplateDAL,
|
certificateTemplateDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
|
pkiSubscriberDAL,
|
||||||
sshCertificateAuthorityDAL,
|
sshCertificateAuthorityDAL,
|
||||||
sshCertificateAuthoritySecretDAL,
|
sshCertificateAuthoritySecretDAL,
|
||||||
sshCertificateDAL,
|
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
|
* Return list of certificate templates for project
|
||||||
*/
|
*/
|
||||||
@@ -1156,17 +1200,15 @@ export const projectServiceFactory = ({
|
|||||||
const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId);
|
const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId);
|
||||||
|
|
||||||
for (const host of hosts) {
|
for (const host of hosts) {
|
||||||
try {
|
const canRead = permission.can(
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionSshHostActions.Read,
|
ProjectPermissionSshHostActions.Read,
|
||||||
subject(ProjectPermissionSub.SshHosts, {
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
hostname: host.hostname
|
hostname: host.hostname
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (canRead) {
|
||||||
allowedHosts.push(host);
|
allowedHosts.push(host);
|
||||||
} catch {
|
|
||||||
// intentionally ignore projects where user lacks access
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1930,6 +1972,7 @@ export const projectServiceFactory = ({
|
|||||||
listProjectSshCas,
|
listProjectSshCas,
|
||||||
listProjectSshHosts,
|
listProjectSshHosts,
|
||||||
listProjectSshHostGroups,
|
listProjectSshHostGroups,
|
||||||
|
listProjectPkiSubscribers,
|
||||||
listProjectSshCertificates,
|
listProjectSshCertificates,
|
||||||
listProjectSshCertificateTemplates,
|
listProjectSshCertificateTemplates,
|
||||||
updateVersionLimit,
|
updateVersionLimit,
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
|||||||
export type TListProjectSshCasDTO = TProjectPermission;
|
export type TListProjectSshCasDTO = TProjectPermission;
|
||||||
export type TListProjectSshHostsDTO = TProjectPermission;
|
export type TListProjectSshHostsDTO = TProjectPermission;
|
||||||
export type TListProjectSshCertificateTemplatesDTO = TProjectPermission;
|
export type TListProjectSshCertificateTemplatesDTO = TProjectPermission;
|
||||||
|
export type TListProjectPkiSubscribersDTO = TProjectPermission;
|
||||||
export type TListProjectSshCertificatesDTO = {
|
export type TListProjectSshCertificatesDTO = {
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ export type TSignCertificateEvent = {
|
|||||||
properties: {
|
properties: {
|
||||||
caId?: string;
|
caId?: string;
|
||||||
certificateTemplateId?: string;
|
certificateTemplateId?: string;
|
||||||
|
subscriberId?: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
};
|
};
|
||||||
@@ -199,6 +200,7 @@ export type TIssueCertificateEvent = {
|
|||||||
properties: {
|
properties: {
|
||||||
caId?: string;
|
caId?: string;
|
||||||
certificateTemplateId?: string;
|
certificateTemplateId?: string;
|
||||||
|
subscriberId?: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/pki/subscribers"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/pki/subscribers/{subscriberName}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Issue Certificate"
|
||||||
|
openapi: "POST /api/v1/pki/subscribers/{subscriberName}/issue-cert"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List Certificates"
|
||||||
|
openapi: "GET /api/v1/pki/subscribers/{subscriberName}/certificates"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Retrieve"
|
||||||
|
openapi: "GET /api/v1/pki/subscribers/{subscriberName}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Sign Certificate"
|
||||||
|
openapi: "POST /api/v1/pki/subscribers/{subscriberName}/sign-certificate"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/pki/subscribers/{subscriberName}"
|
||||||
|
---
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
---
|
---
|
||||||
title: "Add Host"
|
title: "Add Host"
|
||||||
openapi: "POST /api/v1/ssh/host-groups/{sshHostGroupId}/hosts"
|
openapi: "POST /api/v1/ssh/host-groups/{sshHostGroupId}/hosts/{hostId}"
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
---
|
---
|
||||||
title: "Remove Host"
|
title: "Remove Host"
|
||||||
openapi: "DELETE /api/v1/ssh/host-groups/{sshHostGroupId}/hosts/{sshHostId}"
|
openapi: "DELETE /api/v1/ssh/host-groups/{sshHostGroupId}/hosts/{hostId}"
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
---
|
---
|
||||||
title: "List My Hosts"
|
title: "List My Hosts"
|
||||||
openapi: "GET /api/v1/ssh/hosts/"
|
openapi: "GET /api/v1/ssh/hosts"
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
Here's some guidance on each field:
|
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.
|
- 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`.
|
- 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`.
|
- 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.
|
- TTL: The lifetime of the certificate in seconds.
|
||||||
- Key Usage: The key usage extension of the certificate.
|
- Key Usage: The 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
|
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:
|
To check a certificate against the CRL distribution point specified within it with OpenSSL, you can use the following command:
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ sidebarTitle: "Overview"
|
|||||||
description: "Learn how to create a Private CA hierarchy and issue X.509 certificates."
|
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.
|
- [Certificate Authorities](/documentation/platform/pki/private-ca): Create and manage private CAs, including root and intermediate CAs.
|
||||||
- [Certificates](/documentation/platform/pki/certificates): Infisical allows you to issue X.509 certificates using the private CAs you create.
|
- [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.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: "Learn how to create a Private CA hierarchy with Infisical."
|
|||||||
## Concept
|
## Concept
|
||||||
|
|
||||||
The first step to creating your Internal PKI is to create a Private Certificate Authority (CA) hierarchy that is a structure of entities
|
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).
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ graph TD
|
|||||||
|
|
||||||
A typical workflow for setting up a Private CA hierarchy consists of the following steps:
|
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.
|
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.
|
3. Managing the CA lifecycle events such as CA succession.
|
||||||
|
|
||||||
@@ -99,7 +99,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.
|
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.
|
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
|
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.
|
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.
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
@@ -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.
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Issuing CA] --> C1[Certificate]
|
||||||
|
C1 --> S1[Subscriber]
|
||||||
|
A --> C2[Certificate]
|
||||||
|
C2 --> S2[Subscriber]
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 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>
|
||||||
|
Note that this workflow can be executed via the Infisical UI or manually such
|
||||||
|
as via API.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Guide to Issuing Certificates with Subscribers
|
||||||
|
|
||||||
|
In the following steps, we explore how to issue a X.509 certificate for a subscriber.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Creating 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
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.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Requesting a certificate">
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Revoking a Certificate">
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Obtaining a CRL">
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="What is the workflow for renewing a certificate?">
|
||||||
|
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.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 550 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 904 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -112,6 +112,7 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/pki/overview",
|
"documentation/platform/pki/overview",
|
||||||
"documentation/platform/pki/private-ca",
|
"documentation/platform/pki/private-ca",
|
||||||
|
"documentation/platform/pki/subscribers",
|
||||||
"documentation/platform/pki/certificates",
|
"documentation/platform/pki/certificates",
|
||||||
"documentation/platform/pki/pki-issuer",
|
"documentation/platform/pki/pki-issuer",
|
||||||
"documentation/platform/pki/est",
|
"documentation/platform/pki/est",
|
||||||
@@ -1457,6 +1458,18 @@
|
|||||||
{
|
{
|
||||||
"group": "Infisical PKI",
|
"group": "Infisical PKI",
|
||||||
"pages": [
|
"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",
|
"group": "Certificate Authorities",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -287,9 +287,13 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
"/cert-manager/$projectId/ca/$caId",
|
"/cert-manager/$projectId/ca/$caId",
|
||||||
"/_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/ca/$caId"
|
||||||
),
|
),
|
||||||
OverviewPage: setRoute(
|
SubscribersPage: setRoute(
|
||||||
"/cert-manager/$projectId/overview",
|
"/cert-manager/$projectId/subscribers",
|
||||||
"/_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/subscribers"
|
||||||
|
),
|
||||||
|
CertificatesPage: setRoute(
|
||||||
|
"/cert-manager/$projectId/certificates",
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates"
|
||||||
),
|
),
|
||||||
CertificateAuthoritiesPage: setRoute(
|
CertificateAuthoritiesPage: setRoute(
|
||||||
"/cert-manager/$projectId/certificate-authorities",
|
"/cert-manager/$projectId/certificate-authorities",
|
||||||
@@ -302,6 +306,10 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
PkiCollectionDetailsByIDPage: setRoute(
|
PkiCollectionDetailsByIDPage: setRoute(
|
||||||
"/cert-manager/$projectId/pki-collections/$collectionId",
|
"/cert-manager/$projectId/pki-collections/$collectionId",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/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: {
|
Ssh: {
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ export {
|
|||||||
ProjectPermissionIdentityActions,
|
ProjectPermissionIdentityActions,
|
||||||
ProjectPermissionKmipActions,
|
ProjectPermissionKmipActions,
|
||||||
ProjectPermissionMemberActions,
|
ProjectPermissionMemberActions,
|
||||||
|
ProjectPermissionPkiSubscriberActions,
|
||||||
|
ProjectPermissionSshHostActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|||||||
@@ -95,6 +95,15 @@ export enum ProjectPermissionSshHostActions {
|
|||||||
IssueHostCert = "issue-host-cert"
|
IssueHostCert = "issue-host-cert"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionPkiSubscriberActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
IssueCert = "issue-cert",
|
||||||
|
ListCerts = "list-certs"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretRotationActions {
|
export enum ProjectPermissionSecretRotationActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
ReadGeneratedCredentials = "read-generated-credentials",
|
ReadGeneratedCredentials = "read-generated-credentials",
|
||||||
@@ -186,6 +195,7 @@ export enum ProjectPermissionSub {
|
|||||||
SshHostGroups = "ssh-host-groups",
|
SshHostGroups = "ssh-host-groups",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
|
PkiSubscribers = "pki-subscribers",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
Cmek = "cmek",
|
Cmek = "cmek",
|
||||||
SecretSyncs = "secret-syncs",
|
SecretSyncs = "secret-syncs",
|
||||||
@@ -220,6 +230,14 @@ export type SecretRotationSubjectFields = {
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SshHostSubjectFields = {
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PkiSubscriberSubjectFields = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
@@ -282,7 +300,20 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
|
||||||
| [ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts]
|
| [
|
||||||
|
ProjectPermissionSshHostActions,
|
||||||
|
(
|
||||||
|
| ProjectPermissionSub.SshHosts
|
||||||
|
| (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
| [
|
||||||
|
ProjectPermissionPkiSubscriberActions,
|
||||||
|
(
|
||||||
|
| ProjectPermissionSub.PkiSubscribers
|
||||||
|
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
|
||||||
|
)
|
||||||
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export {
|
|||||||
ProjectPermissionIdentityActions,
|
ProjectPermissionIdentityActions,
|
||||||
ProjectPermissionKmipActions,
|
ProjectPermissionKmipActions,
|
||||||
ProjectPermissionMemberActions,
|
ProjectPermissionMemberActions,
|
||||||
|
ProjectPermissionPkiSubscriberActions,
|
||||||
|
ProjectPermissionSshHostActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission
|
useProjectPermission
|
||||||
} from "./ProjectPermissionContext";
|
} from "./ProjectPermissionContext";
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
|
|||||||
return project;
|
return project;
|
||||||
};
|
};
|
||||||
export const getProjectHomePage = (workspace: Workspace) => {
|
export const getProjectHomePage = (workspace: Workspace) => {
|
||||||
|
if (workspace.type === ProjectType.CertificateManager) {
|
||||||
|
return `/${workspace.type}/$projectId/subscribers` as const;
|
||||||
|
}
|
||||||
return `/${workspace.type}/$projectId/overview` as const;
|
return `/${workspace.type}/$projectId/overview` as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { pkiSubscriberKeys } from "../pkiSubscriber/queries";
|
||||||
import { workspaceKeys } from "../workspace";
|
import { workspaceKeys } from "../workspace";
|
||||||
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
|
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
|
||||||
|
|
||||||
@@ -42,6 +43,9 @@ export const useRevokeCert = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug)
|
queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug)
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export * from "./orgAdmin";
|
|||||||
export * from "./organization";
|
export * from "./organization";
|
||||||
export * from "./pkiAlerts";
|
export * from "./pkiAlerts";
|
||||||
export * from "./pkiCollections";
|
export * from "./pkiCollections";
|
||||||
|
export * from "./pkiSubscriber";
|
||||||
export * from "./projectUserAdditionalPrivilege";
|
export * from "./projectUserAdditionalPrivilege";
|
||||||
export * from "./rateLimit";
|
export * from "./rateLimit";
|
||||||
export * from "./roles";
|
export * from "./roles";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export {
|
||||||
|
useCreatePkiSubscriber,
|
||||||
|
useDeletePkiSubscriber,
|
||||||
|
useIssuePkiSubscriberCert,
|
||||||
|
useUpdatePkiSubscriber
|
||||||
|
} from "./mutations";
|
||||||
|
export { useGetPkiSubscriber, useGetPkiSubscriberCertificates } from "./queries";
|
||||||
@@ -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<TPkiSubscriber, object, TCreatePkiSubscriberDTO>({
|
||||||
|
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<TPkiSubscriber, object, TUpdatePkiSubscriberDTO>({
|
||||||
|
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<TPkiSubscriber, object, TDeletePkiSubscriberDTO>({
|
||||||
|
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<TCreateCertificateResponse, object, TIssuePkiSubscriberCertDTO>({
|
||||||
|
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
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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<TPkiSubscriber>(
|
||||||
|
`/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)
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -21,6 +21,7 @@ export type TSshHost = {
|
|||||||
hostCertTtl: string;
|
hostCertTtl: string;
|
||||||
loginMappings: TLoginMapping[];
|
loginMappings: TLoginMapping[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateSshHostDTO = {
|
export type TCreateSshHostDTO = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export {
|
|||||||
useListWorkspaceGroups,
|
useListWorkspaceGroups,
|
||||||
useListWorkspacePkiAlerts,
|
useListWorkspacePkiAlerts,
|
||||||
useListWorkspacePkiCollections,
|
useListWorkspacePkiCollections,
|
||||||
|
useListWorkspacePkiSubscribers,
|
||||||
useListWorkspaceSshCas,
|
useListWorkspaceSshCas,
|
||||||
useListWorkspaceSshCertificates,
|
useListWorkspaceSshCertificates,
|
||||||
useListWorkspaceSshCertificateTemplates,
|
useListWorkspaceSshCertificateTemplates,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { IntegrationAuth } from "../integrationAuth/types";
|
|||||||
import { TIntegration } from "../integrations/types";
|
import { TIntegration } from "../integrations/types";
|
||||||
import { TPkiAlert } from "../pkiAlerts/types";
|
import { TPkiAlert } from "../pkiAlerts/types";
|
||||||
import { TPkiCollection } from "../pkiCollections/types";
|
import { TPkiCollection } from "../pkiCollections/types";
|
||||||
|
import { TPkiSubscriber } from "../pkiSubscriber/types";
|
||||||
import { EncryptedSecret } from "../secrets/types";
|
import { EncryptedSecret } from "../secrets/types";
|
||||||
import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types";
|
import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types";
|
||||||
import { TSshCertificateTemplate } from "../sshCertificateTemplates/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) => {
|
export const useListWorkspaceSshHostGroups = (projectId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId),
|
queryKey: workspaceKeys.getWorkspaceSshHostGroups(projectId),
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const workspaceKeys = {
|
|||||||
}) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const,
|
}) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const,
|
||||||
getWorkspacePkiAlerts: (workspaceId: string) =>
|
getWorkspacePkiAlerts: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-pki-alerts"] as const,
|
[{ workspaceId }, "workspace-pki-alerts"] as const,
|
||||||
|
getWorkspacePkiSubscribers: (projectId: string) =>
|
||||||
|
[{ projectId }, "workspace-pki-subscribers"] as const,
|
||||||
getWorkspacePkiCollections: (workspaceId: string) =>
|
getWorkspacePkiCollections: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-pki-collections"] as const,
|
[{ workspaceId }, "workspace-pki-collections"] as const,
|
||||||
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
||||||
|
|||||||
@@ -104,7 +104,23 @@ export const ProjectLayout = () => {
|
|||||||
{isCertManager && (
|
{isCertManager && (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
to={`/${ProjectType.CertificateManager}/$projectId/overview` as const}
|
to={
|
||||||
|
`/${ProjectType.CertificateManager}/$projectId/subscribers` as const
|
||||||
|
}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive} icon="pki-subscriber">
|
||||||
|
Subscribers
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to={
|
||||||
|
`/${ProjectType.CertificateManager}/$projectId/certificates` as const
|
||||||
|
}
|
||||||
params={{
|
params={{
|
||||||
projectId: currentWorkspace.id
|
projectId: currentWorkspace.id
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ export const AlertingPage = () => {
|
|||||||
<title>{t("common.head-title", { title: "Alerting" })}</title>
|
<title>{t("common.head-title", { title: "Alerting" })}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||||
<PageHeader title="Alerting" />
|
<PageHeader
|
||||||
|
title="Alerting"
|
||||||
|
description="Configure alerts for expiring certificates and CAs to maintain security and compliance."
|
||||||
|
/>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
renderGuardBanner
|
renderGuardBanner
|
||||||
I={ProjectPermissionActions.Read}
|
I={ProjectPermissionActions.Read}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const Page = () => {
|
|||||||
|
|
||||||
handlePopUpClose("deleteCa");
|
handlePopUpClose("deleteCa");
|
||||||
navigate({
|
navigate({
|
||||||
to: `/${ProjectType.CertificateManager}/$projectId/overview` as const,
|
to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const,
|
||||||
params: {
|
params: {
|
||||||
projectId
|
projectId
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -15,7 +15,10 @@ export const CertificateAuthoritiesPage = () => {
|
|||||||
<title>{t("common.head-title", { title: "Certificate Authorities" })}</title>
|
<title>{t("common.head-title", { title: "Certificate Authorities" })}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||||
<PageHeader title="Certificate Authorities" />
|
<PageHeader
|
||||||
|
title="Certificate Authorities"
|
||||||
|
description="Manage internal private certificate authorities for issuing and signing certificates, including root and intermediate CAs."
|
||||||
|
/>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
renderGuardBanner
|
renderGuardBanner
|
||||||
I={ProjectPermissionActions.Read}
|
I={ProjectPermissionActions.Read}
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ export const CertificatesPage = () => {
|
|||||||
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||||
<PageHeader title="Certificates" />
|
<PageHeader
|
||||||
|
title="Certificates"
|
||||||
|
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
|
||||||
|
/>
|
||||||
{/* If both are false, the section does not render. This is to prevent duplicate banners. */}
|
{/* If both are false, the section does not render. This is to prevent duplicate banners. */}
|
||||||
{(canAccessCerts || canAccessPkiColl) && (
|
{(canAccessCerts || canAccessPkiColl) && (
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Friendly Name</Th>
|
<Th>Common Name</Th>
|
||||||
<Th>Status</Th>
|
<Th>Status</Th>
|
||||||
<Th>Not Before</Th>
|
<Th>Not Before</Th>
|
||||||
<Th>Not After</Th>
|
<Th>Not After</Th>
|
||||||
@@ -85,7 +85,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
|
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
|
||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||||
<Td>{certificate.friendlyName}</Td>
|
<Td>{certificate.commonName}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{certificate.status === CertStatus.REVOKED ? (
|
{certificate.status === CertStatus.REVOKED ? (
|
||||||
<Badge variant="danger">Revoked</Badge>
|
<Badge variant="danger">Revoked</Badge>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
|||||||
import { CertificatesPage } from "./CertificatesPage";
|
import { CertificatesPage } from "./CertificatesPage";
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
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
|
component: CertificatesPage
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ export const PkiCollectionPage = () => {
|
|||||||
});
|
});
|
||||||
handlePopUpClose("deletePkiCollection");
|
handlePopUpClose("deletePkiCollection");
|
||||||
navigate({
|
navigate({
|
||||||
to: `/${ProjectType.CertificateManager}/$projectId/overview` as const,
|
to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const,
|
||||||
params: {
|
params: {
|
||||||
projectId
|
projectId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
|
|||||||
{
|
{
|
||||||
label: "Certificate Collections",
|
label: "Certificate Collections",
|
||||||
link: linkOptions({
|
link: linkOptions({
|
||||||
to: "/cert-manager/$projectId/overview",
|
to: "/cert-manager/$projectId/certificates",
|
||||||
params: {
|
params: {
|
||||||
projectId: params.projectId
|
projectId: params.projectId
|
||||||
}
|
}
|
||||||
|
|||||||
+165
@@ -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 (
|
||||||
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
|
{data && (
|
||||||
|
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||||
|
<PageHeader title={data.name}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<Button variant="outline_bg">More</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="p-1">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionPkiSubscriberActions.Delete}
|
||||||
|
a={ProjectPermissionSub.PkiSubscribers}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
isAllowed
|
||||||
|
? "hover:!bg-red-500 hover:!text-white"
|
||||||
|
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("deletePkiSubscriber", {
|
||||||
|
subscriberName: data.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Delete PKI Subscriber
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</PageHeader>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="mr-4 w-96">
|
||||||
|
<PkiSubscriberDetailsSection
|
||||||
|
subscriberName={data.name}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<PkiSubscriberCertificatesSection subscriberName={data.name} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<PkiSubscriberModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deletePkiSubscriber.isOpen}
|
||||||
|
title={`Are you sure you want to remove the PKI subscriber: ${
|
||||||
|
(popUp?.deletePkiSubscriber?.data as { name: string })?.name || ""
|
||||||
|
}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deletePkiSubscriber", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onRemoveSubscriberSubmit(
|
||||||
|
(popUp?.deletePkiSubscriber?.data as { subscriberName: string })?.subscriberName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PkiSubscriberDetailsByIDPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>{t("common.head-title", { title: "PKI Subscriber" })}</title>
|
||||||
|
</Helmet>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionPkiSubscriberActions.Read}
|
||||||
|
a={ProjectPermissionSub.PkiSubscribers}
|
||||||
|
passThrough={false}
|
||||||
|
renderGuardBanner
|
||||||
|
>
|
||||||
|
<Page />
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+27
@@ -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 (
|
||||||
|
<div className="h-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">Certificates</h3>
|
||||||
|
</div>
|
||||||
|
<div className="py-4">
|
||||||
|
<PkiSubscriberCertificatesTable
|
||||||
|
subscriberName={subscriberName}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+176
@@ -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 <Badge variant="danger">Revoked</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <Badge variant="danger">Expired</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysUntilExpiry < 30) {
|
||||||
|
return <Badge variant="primary">Expiring Soon</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Badge variant="success">Valid</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const canListPkiSubscriberCerts = permission.can(
|
||||||
|
ProjectPermissionPkiSubscriberActions.ListCerts,
|
||||||
|
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||||
|
name: subscriberName
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Common Name</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
|
<Th>Not Before</Th>
|
||||||
|
<Th>Not After</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPending && <TableSkeleton columns={5} innerKey="pki-subscriber-certificates" />}
|
||||||
|
{!isPending &&
|
||||||
|
data?.certificates?.map((certificate) => {
|
||||||
|
return (
|
||||||
|
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||||
|
<Td>{certificate.commonName}</Td>
|
||||||
|
<Td>{getCertStatusBadge(certificate.status, certificate.notAfter)}</Td>
|
||||||
|
<Td>
|
||||||
|
{certificate.notBefore
|
||||||
|
? format(new Date(certificate.notBefore), "yyyy-MM-dd")
|
||||||
|
: "-"}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{certificate.notAfter
|
||||||
|
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
|
||||||
|
: "-"}
|
||||||
|
</Td>
|
||||||
|
<Td className="flex justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionPkiSubscriberActions.Delete}
|
||||||
|
a={ProjectPermissionSub.PkiSubscribers}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen &&
|
||||||
|
handlePopUpOpen("revokeCertificate", {
|
||||||
|
serialNumber: certificate.serialNumber
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Revoke Certificate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isPending && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && (
|
||||||
|
<Pagination
|
||||||
|
count={data.totalCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isPending && !data?.certificates?.length && (
|
||||||
|
<EmptyState
|
||||||
|
title={`${canListPkiSubscriberCerts ? "No certificates have been issued for this subscriber" : "You do not have permission to view this subscriber's certificates"}`}
|
||||||
|
icon={faCertificate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+190
@@ -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<TCertificateDetails | null>(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||||
|
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 ? (
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">PKI Subscriber Details</h3>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionPkiSubscriberActions.Edit}
|
||||||
|
a={ProjectPermissionSub.PkiSubscribers}
|
||||||
|
>
|
||||||
|
{(isAllowed) => {
|
||||||
|
return (
|
||||||
|
<Tooltip content="Edit PKI Subscriber">
|
||||||
|
<IconButton
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
ariaLabel="edit icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("pkiSubscriber", {
|
||||||
|
subscriberName: pkiSubscriber.name
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm font-semibold text-mineshaft-300">PKI Subscriber ID</p>
|
||||||
|
<div className="group flex align-top">
|
||||||
|
<p className="text-sm text-mineshaft-300">{pkiSubscriber.id}</p>
|
||||||
|
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||||
|
<Tooltip content={copyTextId}>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(pkiSubscriber.id);
|
||||||
|
setCopyTextId("Copied");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||||
|
<p className="text-sm text-mineshaft-300">{pkiSubscriber.name}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
|
||||||
|
<p className="text-sm text-mineshaft-300">
|
||||||
|
{pkiSubscriberStatusToNameMap[pkiSubscriber.status]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm font-semibold text-mineshaft-300">Common Name</p>
|
||||||
|
<p className="text-sm text-mineshaft-300">{pkiSubscriber.commonName}</p>
|
||||||
|
</div>
|
||||||
|
{canIssuePkiSubscriberCert && (
|
||||||
|
<Button
|
||||||
|
className="mt-4 w-full"
|
||||||
|
colorSchema="primary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isIssuingCert}
|
||||||
|
onClick={() => {
|
||||||
|
onIssuePkiSubscriberCert();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Issue Certificate
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
setIsModalOpen(isOpen);
|
||||||
|
if (!isOpen) {
|
||||||
|
setCertificateDetails(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title="Certificate Details">
|
||||||
|
{certificateDetails && (
|
||||||
|
<CertificateContent
|
||||||
|
serialNumber={certificateDetails.serialNumber}
|
||||||
|
certificate={certificateDetails.certificate}
|
||||||
|
certificateChain={certificateDetails.certificateChain}
|
||||||
|
privateKey={certificateDetails.privateKey}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { PkiSubscriberCertificatesSection } from "./PkiSubscriberCertificatesSection";
|
||||||
|
export { PkiSubscriberDetailsSection } from "./PkiSubscriberDetailsSection";
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>{t("common.head-title", { title: "PKI Subscribers" })}</title>
|
||||||
|
</Helmet>
|
||||||
|
<div className="h-full bg-bunker-800">
|
||||||
|
<div className="container mx-auto flex flex-col justify-between text-white">
|
||||||
|
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||||
|
<PageHeader
|
||||||
|
title="Subscribers"
|
||||||
|
description="Manage subscribers that request and receive certificates, including user devices, servers, and services."
|
||||||
|
/>
|
||||||
|
<PkiSubscriberSection />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<typeof schema>;
|
||||||
|
|
||||||
|
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<FormData>({
|
||||||
|
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 (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.pkiSubscriber?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
reset();
|
||||||
|
handlePopUpToggle("pkiSubscriber", isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={`${pkiSubscriber ? "Update" : "Add"} PKI Subscriber`}>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
{pkiSubscriber && (
|
||||||
|
<FormControl label="Subscriber ID">
|
||||||
|
<Input value={pkiSubscriber.id} isDisabled className="bg-white/[0.07]" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Subscriber Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="web-service" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="caId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Issuing CA"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{(cas || []).map(({ id, dn }) => (
|
||||||
|
<SelectItem value={id} key={`ca-${id}`}>
|
||||||
|
{dn}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="commonName"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Common Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="web.example.com" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="subjectAlternativeNames"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Subject Alternative Names (SANs)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="app1.example.com, app2.example.com, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="ttl"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="TTL"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
|
||||||
|
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||||
|
<div className="order-1 ml-3">Key Usage</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="keyUsages"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Key Usage"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
||||||
|
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={optionValue}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extendedKeyUsages"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Extended Key Usage"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
||||||
|
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={optionValue}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
<div className="mt-4 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{pkiSubscriber ? "Update" : "Add"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("pkiSubscriber", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+151
@@ -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 (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Subscribers</p>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/documentation/platform/pki/subscribers"
|
||||||
|
>
|
||||||
|
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||||
|
Documentation{" "}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.06rem] ml-1 text-xs"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionPkiSubscriberActions.Create}
|
||||||
|
a={ProjectPermissionSub.PkiSubscribers}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("pkiSubscriber")}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
className="ml-4"
|
||||||
|
>
|
||||||
|
Add Subscriber
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PkiSubscribersTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
<PkiSubscriberModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.pkiSubscriberStatus.isOpen}
|
||||||
|
title={`Are you sure you want to ${isEnabling ? "enable" : "disable"} the subscriber ${subscriberName}?`}
|
||||||
|
subTitle={
|
||||||
|
isEnabling
|
||||||
|
? "This action will allow issuing certificates for this subscriber again."
|
||||||
|
: "This action will prevent issuing certificates for this subscriber."
|
||||||
|
}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("pkiSubscriberStatus", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
buttonColorSchema={isEnabling ? "primary" : "danger"}
|
||||||
|
buttonText={isEnabling ? "Enable" : "Disable"}
|
||||||
|
onDeleteApproved={() => onUpdatePkiSubscriberStatus(subscriberStatusData)}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deletePkiSubscriber.isOpen}
|
||||||
|
title="Are you sure you want to remove the PKI subscriber?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deletePkiSubscriber", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onRemovePkiSubscriberSubmit(
|
||||||
|
(popUp?.deletePkiSubscriber?.data as { subscriberName: string })?.subscriberName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+188
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table className="w-full table-fixed">
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
|
<Th>Common Name</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPending && <TableSkeleton columns={4} innerKey="pki-subscribers" />}
|
||||||
|
{!isPending &&
|
||||||
|
data &&
|
||||||
|
data.length > 0 &&
|
||||||
|
data.map((subscriber) => {
|
||||||
|
return (
|
||||||
|
<Tr
|
||||||
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
key={`pki-subscriber-${subscriber.id}`}
|
||||||
|
onClick={() =>
|
||||||
|
navigate({
|
||||||
|
to: `/${ProjectType.CertificateManager}/$projectId/subscribers/$subscriberName` as const,
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
subscriberName: subscriber.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Td>{subscriber.name}</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant={getPkiSubscriberStatusBadgeVariant(subscriber.status)}>
|
||||||
|
{pkiSubscriberStatusToNameMap[subscriber.status]}
|
||||||
|
</Badge>
|
||||||
|
</Td>
|
||||||
|
<Td>{subscriber.commonName}</Td>
|
||||||
|
<Td className="text-right align-middle">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionPkiSubscriberActions.Edit}
|
||||||
|
a={ProjectPermissionSub.PkiSubscribers}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("pkiSubscriber", {
|
||||||
|
subscriberName: subscriber.name
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faPencil} />}
|
||||||
|
>
|
||||||
|
Edit Subscriber
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionPkiSubscriberActions.Edit}
|
||||||
|
a={ProjectPermissionSub.PkiSubscribers}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("pkiSubscriberStatus", {
|
||||||
|
subscriberName: subscriber.name,
|
||||||
|
status:
|
||||||
|
subscriber.status === PkiSubscriberStatus.ACTIVE
|
||||||
|
? PkiSubscriberStatus.DISABLED
|
||||||
|
: PkiSubscriberStatus.ACTIVE
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faBan} />}
|
||||||
|
>
|
||||||
|
{`${subscriber.status === PkiSubscriberStatus.ACTIVE ? "Disable" : "Enable"} Subscriber`}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionPkiSubscriberActions.Delete}
|
||||||
|
a={ProjectPermissionSub.PkiSubscribers}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("deletePkiSubscriber", {
|
||||||
|
subscriberName: subscriber.name
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete Subscriber
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isPending && data?.length === 0 && (
|
||||||
|
<EmptyState title="No PKI subscribers have been added" icon={faUserShield} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PkiSubscriberSection } from "./PkiSubscriberSection";
|
||||||
@@ -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
|
||||||
|
});
|
||||||
@@ -31,7 +31,7 @@ export const Route = createFileRoute(
|
|||||||
{
|
{
|
||||||
label: project.name,
|
label: project.name,
|
||||||
link: linkOptions({
|
link: linkOptions({
|
||||||
to: "/cert-manager/$projectId/overview",
|
to: "/cert-manager/$projectId/subscribers",
|
||||||
params: { projectId: project.id }
|
params: { projectId: project.id }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -58,6 +58,15 @@ export const OrgAdminProjects = withPermission(
|
|||||||
await orgAdminAccessProject.mutateAsync({
|
await orgAdminAccessProject.mutateAsync({
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
if (type === ProjectType.CertificateManager) {
|
||||||
|
await navigate({
|
||||||
|
to: "/cert-manager/$projectId/subscribers" as const,
|
||||||
|
params: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
await navigate({
|
await navigate({
|
||||||
to: `/${type}/$projectId/overview` as const,
|
to: `/${type}/$projectId/overview` as const,
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -1,37 +1,33 @@
|
|||||||
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||||
createFileRoute,
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
linkOptions,
|
import { z } from "zod";
|
||||||
stripSearchParams,
|
|
||||||
} from '@tanstack/react-router'
|
|
||||||
import { zodValidator } from '@tanstack/zod-adapter'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
import { SettingsPage } from './SettingsPage'
|
import { SettingsPage } from "./SettingsPage";
|
||||||
|
|
||||||
const SettingsPageQueryParams = z.object({
|
const SettingsPageQueryParams = z.object({
|
||||||
selectedTab: z.string().catch(''),
|
selectedTab: z.string().catch("")
|
||||||
})
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
'/_authenticate/_inject-org-details/_org-layout/organization/settings/',
|
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
|
||||||
)({
|
)({
|
||||||
component: SettingsPage,
|
component: SettingsPage,
|
||||||
validateSearch: zodValidator(SettingsPageQueryParams),
|
validateSearch: zodValidator(SettingsPageQueryParams),
|
||||||
search: {
|
search: {
|
||||||
middlewares: [stripSearchParams({ selectedTab: '' })],
|
middlewares: [stripSearchParams({ selectedTab: "" })]
|
||||||
},
|
},
|
||||||
context: () => ({
|
context: () => ({
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
label: 'Home',
|
label: "Home",
|
||||||
icon: () => <FontAwesomeIcon icon={faHome} />,
|
icon: () => <FontAwesomeIcon icon={faHome} />,
|
||||||
link: linkOptions({ to: '/' }),
|
link: linkOptions({ to: "/" })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: "Settings"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
});
|
||||||
|
|||||||
+173
@@ -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<TFormSchema>();
|
||||||
|
|
||||||
|
const permissionSubject = ProjectPermissionSub.PkiSubscribers;
|
||||||
|
const items = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: `permissions.${permissionSubject}.${position}.conditions`
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||||
|
<p className="mt-2 text-gray-300">Conditions</p>
|
||||||
|
<p className="text-sm text-mineshaft-400">
|
||||||
|
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||||
|
</p>
|
||||||
|
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||||
|
All conditions must evaluate to true for the policy to take effect.
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
|
{items.fields.map((el, index) => {
|
||||||
|
const condition =
|
||||||
|
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
|
||||||
|
lhs: string;
|
||||||
|
rhs: string;
|
||||||
|
operator: string;
|
||||||
|
}) || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={el.id}
|
||||||
|
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
||||||
|
>
|
||||||
|
<div className="w-1/4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value="name">Name</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-36 items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
asChild
|
||||||
|
content={getConditionOperatorHelperInfo(
|
||||||
|
condition?.operator as PermissionConditionOperators
|
||||||
|
)}
|
||||||
|
className="max-w-xs"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="plus"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="p-2.5"
|
||||||
|
onClick={() => items.remove(index)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
|
||||||
|
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||||
|
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||||
|
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
variant="star"
|
||||||
|
size="xs"
|
||||||
|
className="mt-3"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onClick={() =>
|
||||||
|
items.append({
|
||||||
|
lhs: "name",
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
rhs: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Condition
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+58
-1
@@ -17,6 +17,7 @@ import {
|
|||||||
ProjectPermissionIdentityActions,
|
ProjectPermissionIdentityActions,
|
||||||
ProjectPermissionKmipActions,
|
ProjectPermissionKmipActions,
|
||||||
ProjectPermissionMemberActions,
|
ProjectPermissionMemberActions,
|
||||||
|
ProjectPermissionPkiSubscriberActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSecretRotationActions,
|
ProjectPermissionSecretRotationActions,
|
||||||
ProjectPermissionSecretSyncActions,
|
ProjectPermissionSecretSyncActions,
|
||||||
@@ -131,6 +132,15 @@ const SshHostPolicyActionSchema = z.object({
|
|||||||
[ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional()
|
[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({
|
const SecretRollbackPolicyActionSchema = z.object({
|
||||||
read: z.boolean().optional(),
|
read: z.boolean().optional(),
|
||||||
create: z.boolean().optional()
|
create: z.boolean().optional()
|
||||||
@@ -230,6 +240,12 @@ export const projectRoleFormSchema = z.object({
|
|||||||
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.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.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||||
@@ -271,6 +287,7 @@ type TConditionalFields =
|
|||||||
| ProjectPermissionSub.SecretFolders
|
| ProjectPermissionSub.SecretFolders
|
||||||
| ProjectPermissionSub.SecretImports
|
| ProjectPermissionSub.SecretImports
|
||||||
| ProjectPermissionSub.DynamicSecrets
|
| ProjectPermissionSub.DynamicSecrets
|
||||||
|
| ProjectPermissionSub.PkiSubscribers
|
||||||
| ProjectPermissionSub.SshHosts
|
| ProjectPermissionSub.SshHosts
|
||||||
| ProjectPermissionSub.SecretRotation
|
| ProjectPermissionSub.SecretRotation
|
||||||
| ProjectPermissionSub.Identity;
|
| ProjectPermissionSub.Identity;
|
||||||
@@ -284,7 +301,8 @@ export const isConditionalSubjects = (
|
|||||||
subject === ProjectPermissionSub.SecretFolders ||
|
subject === ProjectPermissionSub.SecretFolders ||
|
||||||
subject === ProjectPermissionSub.Identity ||
|
subject === ProjectPermissionSub.Identity ||
|
||||||
subject === ProjectPermissionSub.SshHosts ||
|
subject === ProjectPermissionSub.SshHosts ||
|
||||||
subject === ProjectPermissionSub.SecretRotation;
|
subject === ProjectPermissionSub.SecretRotation ||
|
||||||
|
subject === ProjectPermissionSub.PkiSubscribers;
|
||||||
|
|
||||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||||
const formConditions: z.infer<typeof ConditionSchema> = [];
|
const formConditions: z.infer<typeof ConditionSchema> = [];
|
||||||
@@ -715,6 +733,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
inverted
|
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;
|
return formVal;
|
||||||
@@ -1104,6 +1149,17 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
{ label: "Remove", value: "delete" }
|
{ 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]: {
|
[ProjectPermissionSub.PkiCollections]: {
|
||||||
title: "PKI Collections",
|
title: "PKI Collections",
|
||||||
actions: [
|
actions: [
|
||||||
@@ -1233,6 +1289,7 @@ const KmsPermissionSubjects = (enabled = false) => ({
|
|||||||
const CertificateManagerPermissionSubjects = (enabled = false) => ({
|
const CertificateManagerPermissionSubjects = (enabled = false) => ({
|
||||||
[ProjectPermissionSub.PkiCollections]: enabled,
|
[ProjectPermissionSub.PkiCollections]: enabled,
|
||||||
[ProjectPermissionSub.PkiAlerts]: enabled,
|
[ProjectPermissionSub.PkiAlerts]: enabled,
|
||||||
|
[ProjectPermissionSub.PkiSubscribers]: enabled,
|
||||||
[ProjectPermissionSub.CertificateAuthorities]: enabled,
|
[ProjectPermissionSub.CertificateAuthorities]: enabled,
|
||||||
[ProjectPermissionSub.CertificateTemplates]: enabled,
|
[ProjectPermissionSub.CertificateTemplates]: enabled,
|
||||||
[ProjectPermissionSub.Certificates]: enabled
|
[ProjectPermissionSub.Certificates]: enabled
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
|||||||
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
||||||
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
|
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
|
||||||
import { PermissionEmptyState } from "./PermissionEmptyState";
|
import { PermissionEmptyState } from "./PermissionEmptyState";
|
||||||
|
import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions";
|
||||||
import {
|
import {
|
||||||
formRolePermission2API,
|
formRolePermission2API,
|
||||||
isConditionalSubjects,
|
isConditionalSubjects,
|
||||||
@@ -59,6 +60,10 @@ export const renderConditionalComponents = (
|
|||||||
return <SshHostPermissionConditions isDisabled={isDisabled} />;
|
return <SshHostPermissionConditions isDisabled={isDisabled} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subject === ProjectPermissionSub.PkiSubscribers) {
|
||||||
|
return <PkiSubscriberPermissionConditions isDisabled={isDisabled} />;
|
||||||
|
}
|
||||||
|
|
||||||
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} 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 { fetchSshHostUserCaPublicKey, useListWorkspaceSshHosts } from "@app/hooks/api";
|
||||||
import { LoginMappingSource } from "@app/hooks/api/sshHost/types";
|
import { LoginMappingSource } from "@app/hooks/api/sshHost/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -221,7 +221,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
Download User CA Public Key
|
Download User CA Public Key
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionSshHostActions.Edit}
|
||||||
a={ProjectPermissionSub.SshHosts}
|
a={ProjectPermissionSub.SshHosts}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
@@ -243,7 +243,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionSshHostActions.Delete}
|
||||||
a={ProjectPermissionSub.SshHosts}
|
a={ProjectPermissionSub.SshHosts}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
+120
-14
@@ -119,8 +119,10 @@ import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/se
|
|||||||
import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route'
|
import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route'
|
||||||
import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route'
|
import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route'
|
||||||
import { Route as organizationAppConnectionsOauthCallbackPageRouteImport } from './pages/organization/AppConnections/OauthCallbackPage/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 certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route'
|
||||||
import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/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 secretManagerIntegrationsWindmillConfigurePageRouteImport } from './pages/secret-manager/integrations/WindmillConfigurePage/route'
|
||||||
import { Route as secretManagerIntegrationsWindmillAuthorizePageRouteImport } from './pages/secret-manager/integrations/WindmillAuthorizePage/route'
|
import { Route as secretManagerIntegrationsWindmillAuthorizePageRouteImport } from './pages/secret-manager/integrations/WindmillAuthorizePage/route'
|
||||||
import { Route as secretManagerIntegrationsVercelConfigurePageRouteImport } from './pages/secret-manager/integrations/VercelConfigurePage/route'
|
import { Route as secretManagerIntegrationsVercelConfigurePageRouteImport } from './pages/secret-manager/integrations/VercelConfigurePage/route'
|
||||||
@@ -251,6 +253,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLa
|
|||||||
createFileRoute(
|
createFileRoute(
|
||||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations',
|
'/_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
|
// Create/Update Routes
|
||||||
|
|
||||||
@@ -855,6 +861,15 @@ const secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute =
|
|||||||
} as any,
|
} as any,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute =
|
||||||
|
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersImport.update(
|
||||||
|
{
|
||||||
|
id: '/subscribers',
|
||||||
|
path: '/subscribers',
|
||||||
|
getParentRoute: () => certManagerLayoutRoute,
|
||||||
|
} as any,
|
||||||
|
)
|
||||||
|
|
||||||
const projectAccessControlPageRouteCertManagerRoute =
|
const projectAccessControlPageRouteCertManagerRoute =
|
||||||
projectAccessControlPageRouteCertManagerImport.update({
|
projectAccessControlPageRouteCertManagerImport.update({
|
||||||
id: '/access-management',
|
id: '/access-management',
|
||||||
@@ -956,8 +971,8 @@ const certManagerSettingsPageRouteRoute =
|
|||||||
|
|
||||||
const certManagerCertificatesPageRouteRoute =
|
const certManagerCertificatesPageRouteRoute =
|
||||||
certManagerCertificatesPageRouteImport.update({
|
certManagerCertificatesPageRouteImport.update({
|
||||||
id: '/overview',
|
id: '/certificates',
|
||||||
path: '/overview',
|
path: '/certificates',
|
||||||
getParentRoute: () => certManagerLayoutRoute,
|
getParentRoute: () => certManagerLayoutRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
@@ -1110,6 +1125,14 @@ const organizationAppConnectionsOauthCallbackPageRouteRoute =
|
|||||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute,
|
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const certManagerPkiSubscriberDetailsByIDPageRouteRoute =
|
||||||
|
certManagerPkiSubscriberDetailsByIDPageRouteImport.update({
|
||||||
|
id: '/$subscriberName',
|
||||||
|
path: '/$subscriberName',
|
||||||
|
getParentRoute: () =>
|
||||||
|
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const certManagerCertAuthDetailsByIDPageRouteRoute =
|
const certManagerCertAuthDetailsByIDPageRouteRoute =
|
||||||
certManagerCertAuthDetailsByIDPageRouteImport.update({
|
certManagerCertAuthDetailsByIDPageRouteImport.update({
|
||||||
id: '/ca/$caId',
|
id: '/ca/$caId',
|
||||||
@@ -1125,6 +1148,14 @@ const secretManagerIntegrationsListPageRouteRoute =
|
|||||||
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute,
|
AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const certManagerPkiSubscribersPageRouteRoute =
|
||||||
|
certManagerPkiSubscribersPageRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () =>
|
||||||
|
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const secretManagerIntegrationsWindmillConfigurePageRouteRoute =
|
const secretManagerIntegrationsWindmillConfigurePageRouteRoute =
|
||||||
secretManagerIntegrationsWindmillConfigurePageRouteImport.update({
|
secretManagerIntegrationsWindmillConfigurePageRouteImport.update({
|
||||||
id: '/windmill/create',
|
id: '/windmill/create',
|
||||||
@@ -2248,10 +2279,10 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof certManagerCertificateAuthoritiesPageRouteImport
|
preLoaderRoute: typeof certManagerCertificateAuthoritiesPageRouteImport
|
||||||
parentRoute: typeof certManagerLayoutImport
|
parentRoute: typeof certManagerLayoutImport
|
||||||
}
|
}
|
||||||
'/_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': {
|
||||||
id: '/_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/certificates'
|
||||||
path: '/overview'
|
path: '/certificates'
|
||||||
fullPath: '/cert-manager/$projectId/overview'
|
fullPath: '/cert-manager/$projectId/certificates'
|
||||||
preLoaderRoute: typeof certManagerCertificatesPageRouteImport
|
preLoaderRoute: typeof certManagerCertificatesPageRouteImport
|
||||||
parentRoute: typeof certManagerLayoutImport
|
parentRoute: typeof certManagerLayoutImport
|
||||||
}
|
}
|
||||||
@@ -2360,6 +2391,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport
|
preLoaderRoute: typeof projectAccessControlPageRouteCertManagerImport
|
||||||
parentRoute: typeof certManagerLayoutImport
|
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': {
|
'/_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'
|
id: '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback'
|
||||||
path: '/azure-app-configuration/oauth2/callback'
|
path: '/azure-app-configuration/oauth2/callback'
|
||||||
@@ -2451,6 +2489,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof projectAccessControlPageRouteSshImport
|
preLoaderRoute: typeof projectAccessControlPageRouteSshImport
|
||||||
parentRoute: typeof sshLayoutImport
|
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/': {
|
'/_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/'
|
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -2465,6 +2510,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport
|
preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport
|
||||||
parentRoute: typeof certManagerLayoutImport
|
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': {
|
'/_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'
|
id: '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback'
|
||||||
path: '/$appConnection/oauth/callback'
|
path: '/$appConnection/oauth/callback'
|
||||||
@@ -3308,12 +3360,31 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren =
|
|||||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren,
|
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren {
|
||||||
|
certManagerPkiSubscribersPageRouteRoute: typeof certManagerPkiSubscribersPageRouteRoute
|
||||||
|
certManagerPkiSubscriberDetailsByIDPageRouteRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren =
|
||||||
|
{
|
||||||
|
certManagerPkiSubscribersPageRouteRoute:
|
||||||
|
certManagerPkiSubscribersPageRouteRoute,
|
||||||
|
certManagerPkiSubscriberDetailsByIDPageRouteRoute:
|
||||||
|
certManagerPkiSubscriberDetailsByIDPageRouteRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren =
|
||||||
|
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute._addFileChildren(
|
||||||
|
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
interface certManagerLayoutRouteChildren {
|
interface certManagerLayoutRouteChildren {
|
||||||
certManagerAlertingPageRouteRoute: typeof certManagerAlertingPageRouteRoute
|
certManagerAlertingPageRouteRoute: typeof certManagerAlertingPageRouteRoute
|
||||||
certManagerCertificateAuthoritiesPageRouteRoute: typeof certManagerCertificateAuthoritiesPageRouteRoute
|
certManagerCertificateAuthoritiesPageRouteRoute: typeof certManagerCertificateAuthoritiesPageRouteRoute
|
||||||
certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute
|
certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute
|
||||||
certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute
|
certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute
|
||||||
projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute
|
projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute
|
||||||
|
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren
|
||||||
certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||||
projectIdentityDetailsByIDPageRouteCertManagerRoute: typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
|
projectIdentityDetailsByIDPageRouteCertManagerRoute: typeof projectIdentityDetailsByIDPageRouteCertManagerRoute
|
||||||
projectMemberDetailsByIDPageRouteCertManagerRoute: typeof projectMemberDetailsByIDPageRouteCertManagerRoute
|
projectMemberDetailsByIDPageRouteCertManagerRoute: typeof projectMemberDetailsByIDPageRouteCertManagerRoute
|
||||||
@@ -3329,6 +3400,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = {
|
|||||||
certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute,
|
certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute,
|
||||||
projectAccessControlPageRouteCertManagerRoute:
|
projectAccessControlPageRouteCertManagerRoute:
|
||||||
projectAccessControlPageRouteCertManagerRoute,
|
projectAccessControlPageRouteCertManagerRoute,
|
||||||
|
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute:
|
||||||
|
AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren,
|
||||||
certManagerCertAuthDetailsByIDPageRouteRoute:
|
certManagerCertAuthDetailsByIDPageRouteRoute:
|
||||||
certManagerCertAuthDetailsByIDPageRouteRoute,
|
certManagerCertAuthDetailsByIDPageRouteRoute,
|
||||||
projectIdentityDetailsByIDPageRouteCertManagerRoute:
|
projectIdentityDetailsByIDPageRouteCertManagerRoute:
|
||||||
@@ -4000,7 +4073,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute
|
'/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute
|
||||||
'/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
|
'/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
|
||||||
'/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
'/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
|
'/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute
|
||||||
'/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
'/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
||||||
'/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
|
'/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
|
||||||
@@ -4016,6 +4089,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
|
'/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
|
||||||
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
|
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
|
||||||
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
|
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
|
||||||
|
'/cert-manager/$projectId/subscribers': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren
|
||||||
'/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
|
'/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
|
||||||
'/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute
|
'/integrations/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute
|
||||||
'/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsRouteBitbucketOauthRedirectRoute
|
'/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsRouteBitbucketOauthRedirectRoute
|
||||||
@@ -4029,8 +4103,10 @@ export interface FileRoutesByFullPath {
|
|||||||
'/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
|
'/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
|
||||||
'/secret-manager/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
|
'/secret-manager/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren
|
||||||
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
|
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
|
||||||
|
'/cert-manager/$projectId/subscribers/': typeof certManagerPkiSubscribersPageRouteRoute
|
||||||
'/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
'/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute
|
||||||
'/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
'/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||||
|
'/cert-manager/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
|
||||||
'/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute
|
'/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute
|
||||||
'/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
'/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||||
'/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
'/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
||||||
@@ -4184,7 +4260,7 @@ export interface FileRoutesByTo {
|
|||||||
'/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute
|
'/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute
|
||||||
'/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
|
'/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
|
||||||
'/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
'/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
|
'/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute
|
||||||
'/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
'/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
||||||
'/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
|
'/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
|
||||||
@@ -4212,8 +4288,10 @@ export interface FileRoutesByTo {
|
|||||||
'/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
|
'/kms/$projectId/access-management': typeof projectAccessControlPageRouteKmsRoute
|
||||||
'/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
|
'/secret-manager/$projectId/access-management': typeof projectAccessControlPageRouteSecretManagerRoute
|
||||||
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
|
'/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute
|
||||||
|
'/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute
|
||||||
'/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute
|
'/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute
|
||||||
'/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
'/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute
|
||||||
|
'/cert-manager/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute
|
||||||
'/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute
|
'/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute
|
||||||
'/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
'/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||||
'/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
'/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
||||||
@@ -4385,7 +4463,7 @@ export interface FileRoutesById {
|
|||||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout': typeof sshLayoutRouteWithChildren
|
'/_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/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/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/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/kmip': typeof kmsKmipPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute
|
||||||
@@ -4401,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/overview': typeof sshSshHostsPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute
|
'/_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/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-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/azure-key-vault/oauth2/callback': typeof secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsRouteBitbucketOauthRedirectRoute
|
'/_authenticate/_inject-org-details/_org-layout/integrations/bitbucket/oauth2/callback': typeof secretManagerIntegrationsRouteBitbucketOauthRedirectRoute
|
||||||
@@ -4414,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/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/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/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/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/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/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/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute
|
||||||
@@ -4579,7 +4660,7 @@ export interface FileRouteTypes {
|
|||||||
| '/organization/ssh/settings'
|
| '/organization/ssh/settings'
|
||||||
| '/cert-manager/$projectId/alerting'
|
| '/cert-manager/$projectId/alerting'
|
||||||
| '/cert-manager/$projectId/certificate-authorities'
|
| '/cert-manager/$projectId/certificate-authorities'
|
||||||
| '/cert-manager/$projectId/overview'
|
| '/cert-manager/$projectId/certificates'
|
||||||
| '/cert-manager/$projectId/settings'
|
| '/cert-manager/$projectId/settings'
|
||||||
| '/kms/$projectId/kmip'
|
| '/kms/$projectId/kmip'
|
||||||
| '/kms/$projectId/overview'
|
| '/kms/$projectId/overview'
|
||||||
@@ -4595,6 +4676,7 @@ export interface FileRouteTypes {
|
|||||||
| '/ssh/$projectId/overview'
|
| '/ssh/$projectId/overview'
|
||||||
| '/ssh/$projectId/settings'
|
| '/ssh/$projectId/settings'
|
||||||
| '/cert-manager/$projectId/access-management'
|
| '/cert-manager/$projectId/access-management'
|
||||||
|
| '/cert-manager/$projectId/subscribers'
|
||||||
| '/integrations/azure-app-configuration/oauth2/callback'
|
| '/integrations/azure-app-configuration/oauth2/callback'
|
||||||
| '/integrations/azure-key-vault/oauth2/callback'
|
| '/integrations/azure-key-vault/oauth2/callback'
|
||||||
| '/integrations/bitbucket/oauth2/callback'
|
| '/integrations/bitbucket/oauth2/callback'
|
||||||
@@ -4608,8 +4690,10 @@ export interface FileRouteTypes {
|
|||||||
| '/secret-manager/$projectId/access-management'
|
| '/secret-manager/$projectId/access-management'
|
||||||
| '/secret-manager/$projectId/integrations'
|
| '/secret-manager/$projectId/integrations'
|
||||||
| '/ssh/$projectId/access-management'
|
| '/ssh/$projectId/access-management'
|
||||||
|
| '/cert-manager/$projectId/subscribers/'
|
||||||
| '/secret-manager/$projectId/integrations/'
|
| '/secret-manager/$projectId/integrations/'
|
||||||
| '/cert-manager/$projectId/ca/$caId'
|
| '/cert-manager/$projectId/ca/$caId'
|
||||||
|
| '/cert-manager/$projectId/subscribers/$subscriberName'
|
||||||
| '/organization/app-connections/$appConnection/oauth/callback'
|
| '/organization/app-connections/$appConnection/oauth/callback'
|
||||||
| '/secret-manager/$projectId/integrations/$integrationId'
|
| '/secret-manager/$projectId/integrations/$integrationId'
|
||||||
| '/secret-manager/$projectId/integrations/select-integration-auth'
|
| '/secret-manager/$projectId/integrations/select-integration-auth'
|
||||||
@@ -4762,7 +4846,7 @@ export interface FileRouteTypes {
|
|||||||
| '/organization/ssh/settings'
|
| '/organization/ssh/settings'
|
||||||
| '/cert-manager/$projectId/alerting'
|
| '/cert-manager/$projectId/alerting'
|
||||||
| '/cert-manager/$projectId/certificate-authorities'
|
| '/cert-manager/$projectId/certificate-authorities'
|
||||||
| '/cert-manager/$projectId/overview'
|
| '/cert-manager/$projectId/certificates'
|
||||||
| '/cert-manager/$projectId/settings'
|
| '/cert-manager/$projectId/settings'
|
||||||
| '/kms/$projectId/kmip'
|
| '/kms/$projectId/kmip'
|
||||||
| '/kms/$projectId/overview'
|
| '/kms/$projectId/overview'
|
||||||
@@ -4790,8 +4874,10 @@ export interface FileRouteTypes {
|
|||||||
| '/kms/$projectId/access-management'
|
| '/kms/$projectId/access-management'
|
||||||
| '/secret-manager/$projectId/access-management'
|
| '/secret-manager/$projectId/access-management'
|
||||||
| '/ssh/$projectId/access-management'
|
| '/ssh/$projectId/access-management'
|
||||||
|
| '/cert-manager/$projectId/subscribers'
|
||||||
| '/secret-manager/$projectId/integrations'
|
| '/secret-manager/$projectId/integrations'
|
||||||
| '/cert-manager/$projectId/ca/$caId'
|
| '/cert-manager/$projectId/ca/$caId'
|
||||||
|
| '/cert-manager/$projectId/subscribers/$subscriberName'
|
||||||
| '/organization/app-connections/$appConnection/oauth/callback'
|
| '/organization/app-connections/$appConnection/oauth/callback'
|
||||||
| '/secret-manager/$projectId/integrations/$integrationId'
|
| '/secret-manager/$projectId/integrations/$integrationId'
|
||||||
| '/secret-manager/$projectId/integrations/select-integration-auth'
|
| '/secret-manager/$projectId/integrations/select-integration-auth'
|
||||||
@@ -4961,7 +5047,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout'
|
| '/_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/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/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/settings'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip'
|
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview'
|
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview'
|
||||||
@@ -4977,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/overview'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings'
|
| '/_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/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-app-configuration/oauth2/callback'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/integrations/azure-key-vault/oauth2/callback'
|
| '/_authenticate/_inject-org-details/_org-layout/integrations/azure-key-vault/oauth2/callback'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/integrations/bitbucket/oauth2/callback'
|
| '/_authenticate/_inject-org-details/_org-layout/integrations/bitbucket/oauth2/callback'
|
||||||
@@ -4990,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/access-management'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations'
|
| '/_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/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/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/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/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/$integrationId'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth'
|
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth'
|
||||||
@@ -5513,9 +5602,10 @@ export const routeTree = rootRoute
|
|||||||
"children": [
|
"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/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/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/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/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/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/identities/$identityId",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId",
|
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId",
|
||||||
@@ -5577,7 +5667,7 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "cert-manager/CertificateAuthoritiesPage/route.tsx",
|
"filePath": "cert-manager/CertificateAuthoritiesPage/route.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
|
"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",
|
"filePath": "cert-manager/CertificatesPage/route.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
|
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
|
||||||
},
|
},
|
||||||
@@ -5641,6 +5731,14 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "project/AccessControlPage/route-cert-manager.tsx",
|
"filePath": "project/AccessControlPage/route-cert-manager.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
|
"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": {
|
"/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback": {
|
||||||
"filePath": "secret-manager/integrations/route-azure-app-configurations-oauth-redirect.tsx",
|
"filePath": "secret-manager/integrations/route-azure-app-configurations-oauth-redirect.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/integrations"
|
"parent": "/_authenticate/_inject-org-details/_org-layout/integrations"
|
||||||
@@ -5773,6 +5871,10 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "project/AccessControlPage/route-ssh.tsx",
|
"filePath": "project/AccessControlPage/route-ssh.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout"
|
"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/": {
|
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/": {
|
||||||
"filePath": "secret-manager/IntegrationsListPage/route.tsx",
|
"filePath": "secret-manager/IntegrationsListPage/route.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations"
|
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations"
|
||||||
@@ -5781,6 +5883,10 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx",
|
"filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
|
"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": {
|
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback": {
|
||||||
"filePath": "organization/AppConnections/OauthCallbackPage/route.tsx",
|
"filePath": "organization/AppConnections/OauthCallbackPage/route.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections"
|
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections"
|
||||||
|
|||||||
@@ -289,7 +289,11 @@ const secretManagerIntegrationsRedirect = route("/integrations", [
|
|||||||
|
|
||||||
const certManagerRoutes = route("/cert-manager/$projectId", [
|
const certManagerRoutes = route("/cert-manager/$projectId", [
|
||||||
layout("cert-manager-layout", "cert-manager/layout.tsx", [
|
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("/certificate-authorities", "cert-manager/CertificateAuthoritiesPage/route.tsx"),
|
||||||
route("/alerting", "cert-manager/AlertingPage/route.tsx"),
|
route("/alerting", "cert-manager/AlertingPage/route.tsx"),
|
||||||
route("/ca/$caId", "cert-manager/CertAuthDetailsByIDPage/route.tsx"),
|
route("/ca/$caId", "cert-manager/CertAuthDetailsByIDPage/route.tsx"),
|
||||||
|
|||||||
Reference in New Issue
Block a user