Merge branch 'main' of https://github.com/Infisical/infisical into feat/suborg-scope-support
@@ -34,6 +34,7 @@ ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
|
|||||||
ARG INTERCOM_ID
|
ARG INTERCOM_ID
|
||||||
ENV VITE_INTERCOM_ID $INTERCOM_ID
|
ENV VITE_INTERCOM_ID $INTERCOM_ID
|
||||||
ARG INFISICAL_PLATFORM_VERSION
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
|
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
Feature: External CA
|
Feature: External CA
|
||||||
|
|
||||||
Scenario: Issue a certificate from an external CA
|
@cloudflare
|
||||||
|
Scenario Outline: Issue a certificate from an external CA with Cloudflare
|
||||||
Given I create a Cloudflare connection as cloudflare
|
Given I create a Cloudflare connection as cloudflare
|
||||||
Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id
|
Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id
|
||||||
Given I create a external ACME CA with the following config as ext_ca
|
Given I create a external ACME CA with the following config as ext_ca
|
||||||
@@ -92,9 +93,7 @@ Feature: External CA
|
|||||||
When I create certificate signing request as csr
|
When I create certificate signing request as csr
|
||||||
Then I add names to certificate signing request csr
|
Then I add names to certificate signing request csr
|
||||||
"""
|
"""
|
||||||
{
|
<subject>
|
||||||
"COMMON_NAME": "localhost"
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
# Pebble has a strict rule to only takes SANs
|
# Pebble has a strict rule to only takes SANs
|
||||||
Then I add subject alternative name to certificate signing request csr
|
Then I add subject alternative name to certificate signing request csr
|
||||||
@@ -178,3 +177,195 @@ Feature: External CA
|
|||||||
"localhost"
|
"localhost"
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| subject |
|
||||||
|
| {"COMMON_NAME": "localhost"} |
|
||||||
|
| {} |
|
||||||
|
|
||||||
|
@dnsme
|
||||||
|
Scenario Outline: Issue a certificate from an external CA with DNS Made Easy
|
||||||
|
Given I create a DNS Made Easy connection as dnsme
|
||||||
|
Then I memorize dnsme with jq ".appConnection.id" as app_conn_id
|
||||||
|
Given I create a external ACME CA with the following config as ext_ca
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"dnsProviderConfig": {
|
||||||
|
"provider": "dns-made-easy",
|
||||||
|
"hostedZoneId": "MOCK_ZONE_ID"
|
||||||
|
},
|
||||||
|
"directoryUrl": "{PEBBLE_URL}",
|
||||||
|
"accountEmail": "fangpen@infisical.com",
|
||||||
|
"dnsAppConnectionId": "{app_conn_id}",
|
||||||
|
"eabKid": "",
|
||||||
|
"eabHmacKey": ""
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
Then I memorize ext_ca with jq ".id" as ext_ca_id
|
||||||
|
Given I create a certificate template with the following config as cert_template
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"subject": [
|
||||||
|
{
|
||||||
|
"type": "common_name",
|
||||||
|
"allowed": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sans": [
|
||||||
|
{
|
||||||
|
"type": "dns_name",
|
||||||
|
"allowed": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keyUsages": {
|
||||||
|
"required": [],
|
||||||
|
"allowed": [
|
||||||
|
"digital_signature",
|
||||||
|
"key_encipherment",
|
||||||
|
"non_repudiation",
|
||||||
|
"data_encipherment",
|
||||||
|
"key_agreement",
|
||||||
|
"key_cert_sign",
|
||||||
|
"crl_sign",
|
||||||
|
"encipher_only",
|
||||||
|
"decipher_only"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extendedKeyUsages": {
|
||||||
|
"required": [],
|
||||||
|
"allowed": [
|
||||||
|
"client_auth",
|
||||||
|
"server_auth",
|
||||||
|
"code_signing",
|
||||||
|
"email_protection",
|
||||||
|
"ocsp_signing",
|
||||||
|
"time_stamping"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"algorithms": {
|
||||||
|
"signature": [
|
||||||
|
"SHA256-RSA",
|
||||||
|
"SHA512-RSA",
|
||||||
|
"SHA384-ECDSA",
|
||||||
|
"SHA384-RSA",
|
||||||
|
"SHA256-ECDSA",
|
||||||
|
"SHA512-ECDSA"
|
||||||
|
],
|
||||||
|
"keyAlgorithm": [
|
||||||
|
"RSA-2048",
|
||||||
|
"RSA-4096",
|
||||||
|
"ECDSA-P384",
|
||||||
|
"RSA-3072",
|
||||||
|
"ECDSA-P256",
|
||||||
|
"ECDSA-P521"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"validity": {
|
||||||
|
"max": "365d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
Then I memorize cert_template with jq ".certificateTemplate.id" as cert_template_id
|
||||||
|
Given I create an ACME profile with ca {ext_ca_id} and template {cert_template_id} as "acme_profile"
|
||||||
|
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||||
|
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||||
|
When I create certificate signing request as csr
|
||||||
|
Then I add names to certificate signing request csr
|
||||||
|
"""
|
||||||
|
<subject>
|
||||||
|
"""
|
||||||
|
# Pebble has a strict rule to only takes SANs
|
||||||
|
Then I add subject alternative name to certificate signing request csr
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
And I create a RSA private key pair as cert_key
|
||||||
|
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
|
||||||
|
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
|
||||||
|
And I select challenge with type http-01 for domain localhost from order in order as challenge
|
||||||
|
And I serve challenge response for challenge at localhost
|
||||||
|
And I tell ACME server that challenge is ready to be verified
|
||||||
|
Given I intercept outgoing requests
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"scope": "https://api.dnsmadeeasy.com:443",
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records",
|
||||||
|
"status": 201,
|
||||||
|
"response": {
|
||||||
|
"gtdLocation": "DEFAULT",
|
||||||
|
"failed": false,
|
||||||
|
"monitor": false,
|
||||||
|
"failover": false,
|
||||||
|
"sourceId": 895364,
|
||||||
|
"dynamicDns": false,
|
||||||
|
"hardLink": false,
|
||||||
|
"ttl": 60,
|
||||||
|
"source": 1,
|
||||||
|
"name": "_acme-challenge",
|
||||||
|
"value": "\"MOCK_HTTP_01_VALUE\"",
|
||||||
|
"id": 12345678,
|
||||||
|
"type": "TXT"
|
||||||
|
},
|
||||||
|
"responseIsBinary": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "https://api.dnsmadeeasy.com:443",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records?type=TXT&recordName=_acme-challenge&page=0",
|
||||||
|
"status": 200,
|
||||||
|
"response": {
|
||||||
|
"totalRecords": 1,
|
||||||
|
"totalPages": 1,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"gtdLocation": "DEFAULT",
|
||||||
|
"failed": false,
|
||||||
|
"monitor": false,
|
||||||
|
"failover": false,
|
||||||
|
"sourceId": 895364,
|
||||||
|
"dynamicDns": false,
|
||||||
|
"hardLink": false,
|
||||||
|
"ttl": 60,
|
||||||
|
"source": 1,
|
||||||
|
"name": "_acme-challenge",
|
||||||
|
"value": "\"MOCK_CHALLENGE_VALUE\"",
|
||||||
|
"id": 1111111,
|
||||||
|
"type": "TXT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"page": 0
|
||||||
|
},
|
||||||
|
"responseIsBinary": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "https://api.dnsmadeeasy.com:443",
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records/1111111",
|
||||||
|
"status": 200,
|
||||||
|
"response": "",
|
||||||
|
"responseIsBinary": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
Then I poll and finalize the ACME order order as finalized_order
|
||||||
|
And the value finalized_order.body with jq ".status" should be equal to "valid"
|
||||||
|
And I parse the full-chain certificate from order finalized_order as cert
|
||||||
|
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| subject |
|
||||||
|
| {"COMMON_NAME": "localhost"} |
|
||||||
|
| {} |
|
||||||
|
|||||||
@@ -147,6 +147,40 @@ def step_impl(context: Context, var_name: str):
|
|||||||
context.vars[var_name] = response
|
context.vars[var_name] = response
|
||||||
|
|
||||||
|
|
||||||
|
@given("I create a DNS Made Easy connection as {var_name}")
|
||||||
|
def step_impl(context: Context, var_name: str):
|
||||||
|
jwt_token = context.vars["AUTH_TOKEN"]
|
||||||
|
conn_slug = faker.slug()
|
||||||
|
with with_nocks(
|
||||||
|
context,
|
||||||
|
definitions=[
|
||||||
|
{
|
||||||
|
"scope": "https://api.dnsmadeeasy.com:443",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/V2.0/dns/managed/",
|
||||||
|
"status": 200,
|
||||||
|
"response": {"totalRecords": 0, "totalPages": 1, "data": [], "page": 0},
|
||||||
|
"responseIsBinary": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
):
|
||||||
|
response = context.http_client.post(
|
||||||
|
"/api/v1/app-connections/dns-made-easy",
|
||||||
|
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||||
|
json={
|
||||||
|
"name": conn_slug,
|
||||||
|
"description": "",
|
||||||
|
"method": "api-key-secret",
|
||||||
|
"credentials": {
|
||||||
|
"apiKey": "MOCK_API_KEY",
|
||||||
|
"secretKey": "MOCK_SECRET_KEY",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
context.vars[var_name] = response
|
||||||
|
|
||||||
|
|
||||||
@given("I create a external ACME CA with the following config as {var_name}")
|
@given("I create a external ACME CA with the following config as {var_name}")
|
||||||
def step_impl(context: Context, var_name: str):
|
def step_impl(context: Context, var_name: str):
|
||||||
jwt_token = context.vars["AUTH_TOKEN"]
|
jwt_token = context.vars["AUTH_TOKEN"]
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasIssuerTypeColumn = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "issuerType");
|
||||||
|
|
||||||
|
if (!hasIssuerTypeColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
|
||||||
|
t.string("issuerType").notNullable().defaultTo("ca");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
|
||||||
|
t.uuid("caId").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasIssuerTypeColumn = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "issuerType");
|
||||||
|
|
||||||
|
if (hasIssuerTypeColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
|
||||||
|
t.dropColumn("issuerType");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const PkiCertificateProfilesSchema = z.object({
|
export const PkiCertificateProfilesSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
caId: z.string().uuid(),
|
caId: z.string().uuid().nullable().optional(),
|
||||||
certificateTemplateId: z.string().uuid(),
|
certificateTemplateId: z.string().uuid(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
@@ -19,7 +19,8 @@ export const PkiCertificateProfilesSchema = z.object({
|
|||||||
apiConfigId: z.string().uuid().nullable().optional(),
|
apiConfigId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
acmeConfigId: z.string().uuid().nullable().optional()
|
acmeConfigId: z.string().uuid().nullable().optional(),
|
||||||
|
issuerType: z.string().default("ca")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TPkiCertificateProfiles = z.infer<typeof PkiCertificateProfilesSchema>;
|
export type TPkiCertificateProfiles = z.infer<typeof PkiCertificateProfilesSchema>;
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...req.body,
|
...req.body,
|
||||||
|
name: req.body.slug,
|
||||||
...req.body.type,
|
...req.body.type,
|
||||||
permissions: req.body.permissions || undefined
|
permissions: req.body.permissions || undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2797,6 +2797,7 @@ interface CreateCertificateProfile {
|
|||||||
name: string;
|
name: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
enrollmentType: string;
|
enrollmentType: string;
|
||||||
|
issuerType: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { applyJitter } from "@app/lib/delay";
|
||||||
import { NotFoundError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
||||||
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
||||||
@@ -15,7 +23,12 @@ import { TDynamicSecretLeaseConfig } from "./dynamic-secret-lease-types";
|
|||||||
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
||||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
|
identityDAL: TIdentityDALFactory;
|
||||||
|
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById" | "findOne">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
|
||||||
@@ -23,18 +36,24 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
|||||||
|
|
||||||
export type TDynamicSecretLeaseQueueServiceFactory = {
|
export type TDynamicSecretLeaseQueueServiceFactory = {
|
||||||
pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise<void>;
|
pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise<void>;
|
||||||
setLeaseRevocation: (leaseId: string, expiryAt: Date) => Promise<void>;
|
setLeaseRevocation: (leaseId: string, dynamicSecretId: string, expiryAt: Date) => Promise<void>;
|
||||||
unsetLeaseRevocation: (leaseId: string) => Promise<void>;
|
unsetLeaseRevocation: (leaseId: string) => Promise<void>;
|
||||||
|
queueFailedRevocation: (leaseId: string, dynamicSecretId: string) => Promise<void>;
|
||||||
init: () => Promise<void>;
|
init: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_REVOCATION_RETRY_COUNT = 10;
|
||||||
|
|
||||||
export const dynamicSecretLeaseQueueServiceFactory = ({
|
export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||||
queueService,
|
queueService,
|
||||||
dynamicSecretDAL,
|
dynamicSecretDAL,
|
||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
dynamicSecretLeaseDAL,
|
dynamicSecretLeaseDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
folderDAL
|
folderDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectDAL,
|
||||||
|
smtpService
|
||||||
}: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => {
|
}: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => {
|
||||||
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
|
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
|
||||||
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
|
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
|
||||||
@@ -48,10 +67,10 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setLeaseRevocation = async (leaseId: string, expiryAt: Date) => {
|
const setLeaseRevocation = async (leaseId: string, dynamicSecretId: string, expiryAt: Date) => {
|
||||||
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
|
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
|
||||||
QueueJobs.DynamicSecretRevocation,
|
QueueJobs.DynamicSecretRevocation,
|
||||||
{ leaseId },
|
{ leaseId, dynamicSecretId },
|
||||||
{
|
{
|
||||||
id: leaseId,
|
id: leaseId,
|
||||||
singletonKey: leaseId,
|
singletonKey: leaseId,
|
||||||
@@ -68,10 +87,53 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
|
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queueFailedRevocation = async (leaseId: string, dynamicSecretId: string) => {
|
||||||
|
const appConfig = getConfig();
|
||||||
|
|
||||||
|
const retryDelaySeconds = appConfig.isDevelopmentMode ? 1 : Math.floor(applyJitter(3_600_000 * 4) / 1000); // retry every 4 hours with 20% +- jitter (convert ms to seconds for pgboss)
|
||||||
|
|
||||||
|
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
|
||||||
|
QueueJobs.DynamicSecretRevocation,
|
||||||
|
{ leaseId, isRetry: true, dynamicSecretId },
|
||||||
|
{
|
||||||
|
singletonKey: `${leaseId}-retry`, // avoid conflicts with scheduled revocation
|
||||||
|
retryDelay: retryDelaySeconds,
|
||||||
|
retryLimit: MAX_REVOCATION_RETRY_COUNT, // we dont want it to ever hit the limit, we want the expireInHours to take effect.
|
||||||
|
expireInHours: 23 // if we set it to 24 hours, pgboss will complain that the expireIn is too high
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $queueDynamicSecretLeaseRevocationFailedEmail = async (leaseId: string, dynamicSecretId: string) => {
|
||||||
|
const appConfig = getConfig();
|
||||||
|
|
||||||
|
const delay = appConfig.isDevelopmentMode ? 1_000 * 60 : 1_000 * 60 * 15; // 1 minute in development, 15 minutes in production
|
||||||
|
|
||||||
|
await queueService.queue(
|
||||||
|
QueueName.DynamicSecretLeaseRevocationFailedEmail,
|
||||||
|
QueueJobs.DynamicSecretLeaseRevocationFailedEmail,
|
||||||
|
{
|
||||||
|
leaseId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jobId: `dynamic-secret-lease-revocation-failed-email-${dynamicSecretId}`,
|
||||||
|
delay,
|
||||||
|
attempts: 3,
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: 1000 * 60 // 1 minute
|
||||||
|
},
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const $dynamicSecretQueueJob = async (
|
const $dynamicSecretQueueJob = async (
|
||||||
jobName: string,
|
jobName: string,
|
||||||
jobId: string,
|
jobId: string,
|
||||||
data: { leaseId: string } | { dynamicSecretCfgId: string }
|
data: { leaseId: string; dynamicSecretId: string; isRetry?: boolean } | { dynamicSecretCfgId: string },
|
||||||
|
retryCount?: number
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
if (jobName === QueueJobs.DynamicSecretRevocation) {
|
if (jobName === QueueJobs.DynamicSecretRevocation) {
|
||||||
@@ -79,7 +141,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
logger.info("Dynamic secret lease revocation started: ", leaseId, jobId);
|
logger.info("Dynamic secret lease revocation started: ", leaseId, jobId);
|
||||||
|
|
||||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||||
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
if (!dynamicSecretLease) {
|
||||||
|
throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
||||||
|
}
|
||||||
|
|
||||||
const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId);
|
const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -150,7 +214,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
}
|
}
|
||||||
logger.info("Finished dynamic secret job", jobId);
|
logger.info("Finished dynamic secret job", jobId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error, "Failed to delete dynamic secret");
|
||||||
|
|
||||||
if (jobName === QueueJobs.DynamicSecretPruning) {
|
if (jobName === QueueJobs.DynamicSecretPruning) {
|
||||||
const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
|
const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
|
||||||
@@ -161,35 +225,119 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (jobName === QueueJobs.DynamicSecretRevocation) {
|
if (jobName === QueueJobs.DynamicSecretRevocation) {
|
||||||
const { leaseId } = data as { leaseId: string };
|
const { leaseId, isRetry, dynamicSecretId } = data as {
|
||||||
|
leaseId: string;
|
||||||
|
isRetry?: boolean;
|
||||||
|
dynamicSecretId: string;
|
||||||
|
};
|
||||||
await dynamicSecretLeaseDAL.updateById(leaseId, {
|
await dynamicSecretLeaseDAL.updateById(leaseId, {
|
||||||
status: DynamicSecretStatus.FailedDeletion,
|
status: DynamicSecretStatus.FailedDeletion,
|
||||||
statusDetails: (error as Error)?.message?.slice(0, 255)
|
statusDetails: `${(error as Error)?.message?.slice(0, 255)} - Retrying automatically`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// only add to retry queue if this is not a retry, and if the error is not a DisableRotationErrors error
|
||||||
|
if (!isRetry && !(error instanceof DisableRotationErrors)) {
|
||||||
|
// if revocation fails, we should stop the job and queue a new job to retry the revocation at a later time.
|
||||||
|
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
|
||||||
|
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
|
||||||
|
await queueFailedRevocation(leaseId, dynamicSecretId);
|
||||||
|
|
||||||
|
// if its the last attempt, and the error isn't a DisableRotationErrors error, send an email to the project admins (debounced)
|
||||||
|
} else if (isRetry && !(error instanceof DisableRotationErrors)) {
|
||||||
|
if (retryCount && retryCount === MAX_REVOCATION_RETRY_COUNT) {
|
||||||
|
// if all retries fail, we should also stop the automatic revocation job.
|
||||||
|
// the ID of the revocation job is set to the leaseId, so we can use that to stop the job
|
||||||
|
|
||||||
|
// we dont have to stop the retry job, because if we hit this point, its the last attempt and the retry job will be stopped by pgboss itself after this point,
|
||||||
|
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
|
||||||
|
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, leaseId);
|
||||||
|
|
||||||
|
await $queueDynamicSecretLeaseRevocationFailedEmail(leaseId, dynamicSecretId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (error instanceof DisableRotationErrors) {
|
if (error instanceof DisableRotationErrors) {
|
||||||
if (jobId) {
|
if (jobId) {
|
||||||
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
|
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
|
||||||
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
|
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
// propogate to next part
|
// propagate to next part
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// send alert email once all revocation attempts have failed
|
||||||
|
const $dynamicSecretLeaseRevocationFailedEmailJob = async (jobId: string, data: { leaseId: string }) => {
|
||||||
|
try {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const { leaseId } = data;
|
||||||
|
logger.info(
|
||||||
|
{ leaseId, jobId },
|
||||||
|
"Dynamic secret revocation failed. Notifying project admins about failed revocation."
|
||||||
|
);
|
||||||
|
|
||||||
|
const lease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||||
|
if (!lease) {
|
||||||
|
throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = await folderDAL.findById(lease.dynamicSecret.folderId);
|
||||||
|
if (!folder) throw new NotFoundError({ message: `Failed to find folder with ${lease.dynamicSecret.folderId}` });
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(folder.projectId);
|
||||||
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(project.id);
|
||||||
|
|
||||||
|
const projectAdmins = projectMembers.filter((member) =>
|
||||||
|
member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
|
||||||
|
);
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||||
|
template: SmtpTemplates.DynamicSecretLeaseRevocationFailed,
|
||||||
|
subjectLine: "Dynamic Secret Lease Revocation Failed",
|
||||||
|
substitutions: {
|
||||||
|
dynamicSecretLeaseUrl: `${appCfg.SITE_URL}/organizations/${project.orgId}/projects/secret-management/${project.id}/secrets/${folder.environment.envSlug}?dynamicSecretId=${lease.dynamicSecret.id}&filterBy=dynamic&search=${lease.dynamicSecret.name}`,
|
||||||
|
dynamicSecretName: lease.dynamicSecret.name,
|
||||||
|
projectName: project.name,
|
||||||
|
environmentSlug: folder.environment.envSlug,
|
||||||
|
errorMessage: lease.statusDetails || "An unknown error occurred"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to send dynamic secret lease revocation failed email");
|
||||||
|
if (error instanceof DisableRotationErrors) {
|
||||||
|
if (jobId) {
|
||||||
|
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretLeaseRevocationFailedEmail, jobId);
|
||||||
|
await queueService.stopJobById(QueueName.DynamicSecretLeaseRevocationFailedEmail, jobId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
|
queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
|
||||||
await $dynamicSecretQueueJob(job.name, job.id as string, job.data);
|
await $dynamicSecretQueueJob(job.name, job.id as string, job.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// we use redis for sending the email because:
|
||||||
|
// 1. we are insensitive to losing the jobs in queue in case of a disaster event
|
||||||
|
// 2. pgboss does not support exclusive job keys on v0.10.x, and upgrading to v0.11.x which supports exclusive jobs comes with a lot of breaking changes, and we would need to manually migrate our existing jobs to the new version
|
||||||
|
queueService.start(QueueName.DynamicSecretLeaseRevocationFailedEmail, async (job) => {
|
||||||
|
await $dynamicSecretLeaseRevocationFailedEmailJob(job.id as string, job.data);
|
||||||
|
});
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
await queueService.startPg<QueueName.DynamicSecretRevocation>(
|
await queueService.startPg<QueueName.DynamicSecretRevocation>(
|
||||||
QueueJobs.DynamicSecretRevocation,
|
QueueJobs.DynamicSecretRevocation,
|
||||||
async ([job]) => {
|
async ([job]) => {
|
||||||
await $dynamicSecretQueueJob(job.name, job.id, job.data);
|
await $dynamicSecretQueueJob(job.name, job.id, job.data, job.retryCount);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
workerCount: 5,
|
workerCount: 10,
|
||||||
pollingIntervalSeconds: 1
|
pollingIntervalSeconds: 1
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -210,6 +358,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
pruneDynamicSecret,
|
pruneDynamicSecret,
|
||||||
setLeaseRevocation,
|
setLeaseRevocation,
|
||||||
unsetLeaseRevocation,
|
unsetLeaseRevocation,
|
||||||
|
queueFailedRevocation,
|
||||||
init
|
init
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
config
|
config
|
||||||
});
|
});
|
||||||
|
|
||||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
|
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, dynamicSecretCfg.id, expireAt);
|
||||||
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
|
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
||||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
|
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, dynamicSecretCfg.id, expireAt);
|
||||||
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
||||||
expireAt,
|
expireAt,
|
||||||
externalEntityId: entityId
|
externalEntityId: entityId
|
||||||
@@ -358,11 +358,13 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
if ((revokeResponse as { error?: Error })?.error) {
|
if ((revokeResponse as { error?: Error })?.error) {
|
||||||
const { error } = revokeResponse as { error?: Error };
|
const { error } = revokeResponse as { error?: Error };
|
||||||
logger.error(error?.message, "Failed to revoke lease");
|
logger.error(error?.message, "Failed to revoke lease");
|
||||||
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
||||||
status: DynamicSecretLeaseStatus.FailedDeletion,
|
status: DynamicSecretLeaseStatus.FailedDeletion,
|
||||||
statusDetails: error?.message?.slice(0, 255)
|
statusDetails: error?.message?.slice(0, 255)
|
||||||
});
|
});
|
||||||
return deletedDynamicSecretLease;
|
// queue a job to retry the revocation at a later time
|
||||||
|
await dynamicSecretQueueService.queueFailedRevocation(dynamicSecretLease.id, dynamicSecretCfg.id);
|
||||||
|
return updatedDynamicSecretLease;
|
||||||
}
|
}
|
||||||
|
|
||||||
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const isOfflineLicenseKey = (licenseKey: string): boolean => {
|
|||||||
|
|
||||||
return "signature" in contents && "license" in contents;
|
return "signature" in contents && "license" in contents;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export const getLicenseKeyConfig = (
|
|||||||
const cfg = config || getConfig();
|
const cfg = config || getConfig();
|
||||||
|
|
||||||
if (!cfg) {
|
if (!cfg) {
|
||||||
return { isValid: true };
|
return { isValid: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const licenseKey = cfg.LICENSE_KEY;
|
const licenseKey = cfg.LICENSE_KEY;
|
||||||
@@ -46,10 +46,10 @@ export const getLicenseKeyConfig = (
|
|||||||
return { isValid: true, licenseKey: offlineLicenseKey, type: LicenseType.Offline };
|
return { isValid: true, licenseKey: offlineLicenseKey, type: LicenseType.Offline };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isValid: true };
|
return { isValid: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isValid: true };
|
return { isValid: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||||
@@ -64,56 +64,56 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
environmentsUsed: 0,
|
environmentsUsed: 0,
|
||||||
identityLimit: null,
|
identityLimit: null,
|
||||||
identitiesUsed: 0,
|
identitiesUsed: 0,
|
||||||
dynamicSecret: true,
|
dynamicSecret: false,
|
||||||
secretVersioning: true,
|
secretVersioning: true,
|
||||||
pitRecovery: true,
|
pitRecovery: false,
|
||||||
ipAllowlisting: true,
|
ipAllowlisting: false,
|
||||||
rbac: true,
|
rbac: false,
|
||||||
githubOrgSync: true,
|
githubOrgSync: false,
|
||||||
customRateLimits: true,
|
customRateLimits: false,
|
||||||
subOrganization: true,
|
subOrganization: false,
|
||||||
customAlerts: true,
|
customAlerts: false,
|
||||||
secretAccessInsights: true,
|
secretAccessInsights: false,
|
||||||
auditLogs: true,
|
auditLogs: false,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
auditLogStreams: true,
|
auditLogStreams: false,
|
||||||
auditLogStreamLimit: 3,
|
auditLogStreamLimit: 3,
|
||||||
samlSSO: true,
|
samlSSO: false,
|
||||||
enforceGoogleSSO: true,
|
enforceGoogleSSO: false,
|
||||||
hsm: true,
|
hsm: false,
|
||||||
oidcSSO: true,
|
oidcSSO: false,
|
||||||
scim: true,
|
scim: false,
|
||||||
ldap: true,
|
ldap: false,
|
||||||
groups: true,
|
groups: false,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: true,
|
secretApproval: false,
|
||||||
secretRotation: true,
|
secretRotation: false,
|
||||||
caCrl: true,
|
caCrl: false,
|
||||||
instanceUserManagement: true,
|
instanceUserManagement: false,
|
||||||
externalKms: true,
|
externalKms: false,
|
||||||
rateLimits: {
|
rateLimits: {
|
||||||
readLimit: 60,
|
readLimit: 60,
|
||||||
writeLimit: 200,
|
writeLimit: 200,
|
||||||
secretsLimit: 40
|
secretsLimit: 40
|
||||||
},
|
},
|
||||||
pkiEst: true,
|
pkiEst: false,
|
||||||
pkiAcme: true,
|
pkiAcme: false,
|
||||||
enforceMfa: true,
|
enforceMfa: false,
|
||||||
projectTemplates: true,
|
projectTemplates: false,
|
||||||
kmip: true,
|
kmip: false,
|
||||||
gateway: true,
|
gateway: false,
|
||||||
sshHostGroups: true,
|
sshHostGroups: false,
|
||||||
secretScanning: true,
|
secretScanning: false,
|
||||||
enterpriseSecretSyncs: true,
|
enterpriseSecretSyncs: false,
|
||||||
enterpriseCertificateSyncs: true,
|
enterpriseCertificateSyncs: false,
|
||||||
enterpriseAppConnections: true,
|
enterpriseAppConnections: false,
|
||||||
fips: true,
|
fips: false,
|
||||||
eventSubscriptions: true,
|
eventSubscriptions: false,
|
||||||
machineIdentityAuthTemplates: true,
|
machineIdentityAuthTemplates: false,
|
||||||
pkiLegacyTemplates: true,
|
pkiLegacyTemplates: false,
|
||||||
pam: true
|
pam: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (
|
export const setupLicenseRequestWithStore = (
|
||||||
|
|||||||
@@ -683,6 +683,13 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
payload: TFinalizeAcmeOrderPayload;
|
payload: TFinalizeAcmeOrderPayload;
|
||||||
}): Promise<TAcmeResponse<TAcmeOrderResource>> => {
|
}): Promise<TAcmeResponse<TAcmeOrderResource>> => {
|
||||||
const profile = (await certificateProfileDAL.findByIdWithConfigs(profileId))!;
|
const profile = (await certificateProfileDAL.findByIdWithConfigs(profileId))!;
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for ACME enrollment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId);
|
let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId);
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new NotFoundError({ message: "ACME order not found" });
|
throw new NotFoundError({ message: "ACME order not found" });
|
||||||
@@ -729,7 +736,7 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name + SANs mismatch with order identifiers" });
|
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name + SANs mismatch with order identifiers" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId!);
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
throw new NotFoundError({ message: "Certificate Authority not found" });
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
}
|
}
|
||||||
@@ -769,7 +776,9 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
const cert = await orderCertificate(
|
const cert = await orderCertificate(
|
||||||
{
|
{
|
||||||
caId: certificateAuthority!.id,
|
caId: certificateAuthority!.id,
|
||||||
commonName: certificateRequest.commonName!,
|
// It is possible that the CSR does not have a common name, in which case we use an empty string
|
||||||
|
// (more likely than not for a CSR from a modern ACME client like certbot, cert-manager, etc.)
|
||||||
|
commonName: certificateRequest.commonName ?? "",
|
||||||
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
|
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
|
||||||
csr: Buffer.from(csrPem),
|
csr: Buffer.from(csrPem),
|
||||||
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
|
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ const envSchema = z
|
|||||||
})
|
})
|
||||||
.default("{}")
|
.default("{}")
|
||||||
),
|
),
|
||||||
|
DNS_MADE_EASY_SANDBOX_ENABLED: zodStrBool.default("false").optional(),
|
||||||
// smtp options
|
// smtp options
|
||||||
SMTP_HOST: zpStr(z.string().optional()),
|
SMTP_HOST: zpStr(z.string().optional()),
|
||||||
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
||||||
|
|||||||
@@ -2,3 +2,13 @@ export const delay = (ms: number) =>
|
|||||||
new Promise<void>((resolve) => {
|
new Promise<void>((resolve) => {
|
||||||
setTimeout(resolve, ms);
|
setTimeout(resolve, ms);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const applyJitter = (delayMs: number) => {
|
||||||
|
const jitterFactor = 0.2;
|
||||||
|
|
||||||
|
// generates random value in [-0.2, +0.2] range
|
||||||
|
const randomFactor = (Math.random() * 2 - 1) * jitterFactor;
|
||||||
|
const jitterAmount = randomFactor * delayMs;
|
||||||
|
|
||||||
|
return delayMs + jitterAmount;
|
||||||
|
};
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export enum QueueName {
|
|||||||
SecretPushEventScan = "secret-push-event-scan",
|
SecretPushEventScan = "secret-push-event-scan",
|
||||||
UpgradeProjectToGhost = "upgrade-project-to-ghost",
|
UpgradeProjectToGhost = "upgrade-project-to-ghost",
|
||||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||||
|
DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email",
|
||||||
CaCrlRotation = "ca-crl-rotation",
|
CaCrlRotation = "ca-crl-rotation",
|
||||||
CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber
|
CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber
|
||||||
SecretReplication = "secret-replication",
|
SecretReplication = "secret-replication",
|
||||||
@@ -120,6 +121,7 @@ export enum QueueJobs {
|
|||||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||||
CreateFolderTreeCheckpoint = "create-folder-tree-checkpoint",
|
CreateFolderTreeCheckpoint = "create-folder-tree-checkpoint",
|
||||||
|
DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email",
|
||||||
InvalidateCache = "invalidate-cache",
|
InvalidateCache = "invalidate-cache",
|
||||||
SecretScanningV2FullScan = "secret-scanning-v2-full-scan",
|
SecretScanningV2FullScan = "secret-scanning-v2-full-scan",
|
||||||
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan",
|
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan",
|
||||||
@@ -219,11 +221,19 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.TelemetryInstanceStats;
|
name: QueueJobs.TelemetryInstanceStats;
|
||||||
payload: undefined;
|
payload: undefined;
|
||||||
};
|
};
|
||||||
|
[QueueName.DynamicSecretLeaseRevocationFailedEmail]: {
|
||||||
|
name: QueueJobs.DynamicSecretLeaseRevocationFailedEmail;
|
||||||
|
payload: {
|
||||||
|
leaseId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
[QueueName.DynamicSecretRevocation]:
|
[QueueName.DynamicSecretRevocation]:
|
||||||
| {
|
| {
|
||||||
name: QueueJobs.DynamicSecretRevocation;
|
name: QueueJobs.DynamicSecretRevocation;
|
||||||
payload: {
|
payload: {
|
||||||
|
isRetry?: boolean;
|
||||||
leaseId: string;
|
leaseId: string;
|
||||||
|
dynamicSecretId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ export const registerServeUI = async (
|
|||||||
const frontendPath = path.join(dir, frontendName);
|
const frontendPath = path.join(dir, frontendName);
|
||||||
await server.register(staticServe, {
|
await server.register(staticServe, {
|
||||||
root: frontendPath,
|
root: frontendPath,
|
||||||
wildcard: false
|
wildcard: false,
|
||||||
|
maxAge: "30d",
|
||||||
|
immutable: true
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
@@ -58,11 +60,12 @@ export const registerServeUI = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should help avoid caching any chunks (temp fix)
|
return reply.sendFile("index.html", {
|
||||||
void reply.header("Cache-Control", "no-cache, no-store, must-revalidate, private, max-age=0");
|
immutable: false,
|
||||||
void reply.header("Pragma", "no-cache");
|
maxAge: 0,
|
||||||
void reply.header("Expires", "0");
|
lastModified: false,
|
||||||
return reply.sendFile("index.html");
|
etag: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1874,7 +1874,12 @@ export const registerRoutes = async (
|
|||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
dynamicSecretDAL,
|
dynamicSecretDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
smtpService,
|
||||||
|
userDAL,
|
||||||
|
identityDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectDAL
|
||||||
});
|
});
|
||||||
const dynamicSecretService = dynamicSecretServiceFactory({
|
const dynamicSecretService = dynamicSecretServiceFactory({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -2219,7 +2224,10 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
certificateSyncDAL,
|
certificateSyncDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
certificateBodyDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateV3Queue = certificateV3QueueServiceFactory({
|
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ import {
|
|||||||
DigitalOceanConnectionListItemSchema,
|
DigitalOceanConnectionListItemSchema,
|
||||||
SanitizedDigitalOceanConnectionSchema
|
SanitizedDigitalOceanConnectionSchema
|
||||||
} from "@app/services/app-connection/digital-ocean";
|
} from "@app/services/app-connection/digital-ocean";
|
||||||
|
import {
|
||||||
|
DNSMadeEasyConnectionListItemSchema,
|
||||||
|
SanitizedDNSMadeEasyConnectionSchema
|
||||||
|
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
|
||||||
import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
|
import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
|
||||||
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||||
@@ -170,7 +174,8 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedAzureADCSConnectionSchema.options,
|
...SanitizedAzureADCSConnectionSchema.options,
|
||||||
...SanitizedRedisConnectionSchema.options,
|
...SanitizedRedisConnectionSchema.options,
|
||||||
...SanitizedLaravelForgeConnectionSchema.options,
|
...SanitizedLaravelForgeConnectionSchema.options,
|
||||||
...SanitizedChefConnectionSchema.options
|
...SanitizedChefConnectionSchema.options,
|
||||||
|
...SanitizedDNSMadeEasyConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
@@ -215,7 +220,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
AzureADCSConnectionListItemSchema,
|
AzureADCSConnectionListItemSchema,
|
||||||
RedisConnectionListItemSchema,
|
RedisConnectionListItemSchema,
|
||||||
LaravelForgeConnectionListItemSchema,
|
LaravelForgeConnectionListItemSchema,
|
||||||
ChefConnectionListItemSchema
|
ChefConnectionListItemSchema,
|
||||||
|
DNSMadeEasyConnectionListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateDNSMadeEasyConnectionSchema,
|
||||||
|
SanitizedDNSMadeEasyConnectionSchema,
|
||||||
|
UpdateDNSMadeEasyConnectionSchema
|
||||||
|
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerDNSMadeEasyConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.DNSMadeEasy,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedDNSMadeEasyConnectionSchema,
|
||||||
|
createSchema: CreateDNSMadeEasyConnectionSchema,
|
||||||
|
updateSchema: UpdateDNSMadeEasyConnectionSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
// The below endpoints are not exposed and for Infisical App use
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/dns-made-easy-zones`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
const zones = await server.services.appConnection.dnsMadeEasy.listZones(connectionId, req.permission);
|
||||||
|
return zones;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -16,6 +16,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
|||||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||||
|
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
|
||||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
||||||
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
|
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
|
||||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||||
@@ -78,6 +79,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
|||||||
[AppConnection.Flyio]: registerFlyioConnectionRouter,
|
[AppConnection.Flyio]: registerFlyioConnectionRouter,
|
||||||
[AppConnection.GitLab]: registerGitLabConnectionRouter,
|
[AppConnection.GitLab]: registerGitLabConnectionRouter,
|
||||||
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
|
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
|
||||||
|
[AppConnection.DNSMadeEasy]: registerDNSMadeEasyConnectionRouter,
|
||||||
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
|
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
|
||||||
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
|
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
|
||||||
[AppConnection.Railway]: registerRailwayConnectionRouter,
|
[AppConnection.Railway]: registerRailwayConnectionRouter,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
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 { CertStatus } from "@app/services/certificate/certificate-types";
|
import { CertStatus } from "@app/services/certificate/certificate-types";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
|
|
||||||
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -23,7 +23,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
caId: z.string().uuid(),
|
caId: z.string().uuid().optional(),
|
||||||
certificateTemplateId: z.string().uuid(),
|
certificateTemplateId: z.string().uuid(),
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
@@ -32,6 +32,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType),
|
enrollmentType: z.nativeEnum(EnrollmentType),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).default(IssuerType.CA),
|
||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
@@ -50,43 +51,100 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.enrollmentType === EnrollmentType.EST) {
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
if (!data.estConfig) {
|
return !!data.estConfig;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === EnrollmentType.API) {
|
|
||||||
if (!data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.estConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === EnrollmentType.ACME) {
|
|
||||||
if (!data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.estConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
message: "EST enrollment type requires EST configuration"
|
||||||
"EST enrollment type requires EST configuration and cannot have API or ACME configuration. API enrollment type requires API configuration and cannot have EST or ACME configuration. ACME enrollment type requires ACME configuration and cannot have EST or API configuration."
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !!data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type requires API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !!data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type requires ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
|
return !data.apiConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "EST enrollment type cannot have API or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !data.estConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type cannot have EST or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !data.estConfig && !data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type cannot have EST or API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.CA) {
|
||||||
|
return !!data.caId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "CA issuer type requires a CA ID"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return !data.caId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type cannot have a CA ID"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return data.enrollmentType === EnrollmentType.API;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type only supports API enrollment"
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
response: {
|
response: {
|
||||||
@@ -115,7 +173,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
certificateProfileId: certificateProfile.id,
|
certificateProfileId: certificateProfile.id,
|
||||||
name: certificateProfile.slug,
|
name: certificateProfile.slug,
|
||||||
projectId: certificateProfile.projectId,
|
projectId: certificateProfile.projectId,
|
||||||
enrollmentType: certificateProfile.enrollmentType
|
enrollmentType: certificateProfile.enrollmentType,
|
||||||
|
issuerType: certificateProfile.issuerType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -139,6 +198,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
limit: z.coerce.number().min(1).max(100).default(20),
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).optional(),
|
||||||
caId: z.string().uuid().optional()
|
caId: z.string().uuid().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -339,6 +399,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.optional(),
|
.optional(),
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).optional(),
|
||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export enum AppConnection {
|
|||||||
Flyio = "flyio",
|
Flyio = "flyio",
|
||||||
GitLab = "gitlab",
|
GitLab = "gitlab",
|
||||||
Cloudflare = "cloudflare",
|
Cloudflare = "cloudflare",
|
||||||
|
DNSMadeEasy = "dns-made-easy",
|
||||||
Zabbix = "zabbix",
|
Zabbix = "zabbix",
|
||||||
Railway = "railway",
|
Railway = "railway",
|
||||||
Bitbucket = "bitbucket",
|
Bitbucket = "bitbucket",
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ import {
|
|||||||
getDigitalOceanConnectionListItem,
|
getDigitalOceanConnectionListItem,
|
||||||
validateDigitalOceanConnectionCredentials
|
validateDigitalOceanConnectionCredentials
|
||||||
} from "./digital-ocean";
|
} from "./digital-ocean";
|
||||||
|
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy/dns-made-easy-connection-enum";
|
||||||
|
import {
|
||||||
|
getDNSMadeEasyConnectionListItem,
|
||||||
|
validateDNSMadeEasyConnectionCredentials
|
||||||
|
} from "./dns-made-easy/dns-made-easy-connection-fns";
|
||||||
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
|
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
|
||||||
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
||||||
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
||||||
@@ -171,7 +176,8 @@ const PKI_APP_CONNECTIONS = [
|
|||||||
AppConnection.Cloudflare,
|
AppConnection.Cloudflare,
|
||||||
AppConnection.AzureADCS,
|
AppConnection.AzureADCS,
|
||||||
AppConnection.AzureKeyVault,
|
AppConnection.AzureKeyVault,
|
||||||
AppConnection.Chef
|
AppConnection.Chef,
|
||||||
|
AppConnection.DNSMadeEasy
|
||||||
];
|
];
|
||||||
|
|
||||||
export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||||
@@ -207,6 +213,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
|||||||
getFlyioConnectionListItem(),
|
getFlyioConnectionListItem(),
|
||||||
getGitLabConnectionListItem(),
|
getGitLabConnectionListItem(),
|
||||||
getCloudflareConnectionListItem(),
|
getCloudflareConnectionListItem(),
|
||||||
|
getDNSMadeEasyConnectionListItem(),
|
||||||
getZabbixConnectionListItem(),
|
getZabbixConnectionListItem(),
|
||||||
getRailwayConnectionListItem(),
|
getRailwayConnectionListItem(),
|
||||||
getBitbucketConnectionListItem(),
|
getBitbucketConnectionListItem(),
|
||||||
@@ -339,6 +346,7 @@ export const validateAppConnectionCredentials = async (
|
|||||||
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.DNSMadeEasy]: validateDNSMadeEasyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
@@ -395,6 +403,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case OktaConnectionMethod.ApiToken:
|
case OktaConnectionMethod.ApiToken:
|
||||||
case LaravelForgeConnectionMethod.ApiToken:
|
case LaravelForgeConnectionMethod.ApiToken:
|
||||||
return "API Token";
|
return "API Token";
|
||||||
|
case DNSMadeEasyConnectionMethod.APIKeySecret:
|
||||||
|
return "API Key & Secret";
|
||||||
case PostgresConnectionMethod.UsernameAndPassword:
|
case PostgresConnectionMethod.UsernameAndPassword:
|
||||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||||
case MySqlConnectionMethod.UsernameAndPassword:
|
case MySqlConnectionMethod.UsernameAndPassword:
|
||||||
@@ -483,6 +493,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
|
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
|
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
|
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.DNSMadeEasy]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
|
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
|
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
|
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.Flyio]: "Fly.io",
|
[AppConnection.Flyio]: "Fly.io",
|
||||||
[AppConnection.GitLab]: "GitLab",
|
[AppConnection.GitLab]: "GitLab",
|
||||||
[AppConnection.Cloudflare]: "Cloudflare",
|
[AppConnection.Cloudflare]: "Cloudflare",
|
||||||
|
[AppConnection.DNSMadeEasy]: "DNS Made Easy",
|
||||||
[AppConnection.Zabbix]: "Zabbix",
|
[AppConnection.Zabbix]: "Zabbix",
|
||||||
[AppConnection.Railway]: "Railway",
|
[AppConnection.Railway]: "Railway",
|
||||||
[AppConnection.Bitbucket]: "Bitbucket",
|
[AppConnection.Bitbucket]: "Bitbucket",
|
||||||
@@ -77,6 +78,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
|||||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
|
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
|
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
|
||||||
|
[AppConnection.DNSMadeEasy]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
|
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Railway]: AppConnectionPlanType.Regular,
|
[AppConnection.Railway]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
|
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
|||||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||||
|
import { ValidateDNSMadeEasyConnectionCredentialsSchema } from "./dns-made-easy/dns-made-easy-connection-schema";
|
||||||
|
import { dnsMadeEasyConnectionService } from "./dns-made-easy/dns-made-easy-connection-service";
|
||||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||||
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
||||||
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
|
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
|
||||||
@@ -167,6 +169,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
||||||
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
|
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
|
||||||
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
|
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
|
||||||
|
[AppConnection.DNSMadeEasy]: ValidateDNSMadeEasyConnectionCredentialsSchema,
|
||||||
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
|
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
|
||||||
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
|
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
|
||||||
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
|
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
|
||||||
@@ -875,6 +878,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
flyio: flyioConnectionService(connectAppConnectionById),
|
flyio: flyioConnectionService(connectAppConnectionById),
|
||||||
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
cloudflare: cloudflareConnectionService(connectAppConnectionById),
|
cloudflare: cloudflareConnectionService(connectAppConnectionById),
|
||||||
|
dnsMadeEasy: dnsMadeEasyConnectionService(connectAppConnectionById),
|
||||||
zabbix: zabbixConnectionService(connectAppConnectionById),
|
zabbix: zabbixConnectionService(connectAppConnectionById),
|
||||||
railway: railwayConnectionService(connectAppConnectionById),
|
railway: railwayConnectionService(connectAppConnectionById),
|
||||||
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
TOracleDBConnectionInput,
|
TOracleDBConnectionInput,
|
||||||
TValidateOracleDBConnectionCredentialsSchema
|
TValidateOracleDBConnectionCredentialsSchema
|
||||||
} from "@app/ee/services/app-connections/oracledb";
|
} from "@app/ee/services/app-connections/oracledb";
|
||||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
|
||||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
@@ -106,6 +106,12 @@ import {
|
|||||||
TDigitalOceanConnectionInput,
|
TDigitalOceanConnectionInput,
|
||||||
TValidateDigitalOceanCredentialsSchema
|
TValidateDigitalOceanCredentialsSchema
|
||||||
} from "./digital-ocean";
|
} from "./digital-ocean";
|
||||||
|
import {
|
||||||
|
TDNSMadeEasyConnection,
|
||||||
|
TDNSMadeEasyConnectionConfig,
|
||||||
|
TDNSMadeEasyConnectionInput,
|
||||||
|
TValidateDNSMadeEasyConnectionCredentialsSchema
|
||||||
|
} from "./dns-made-easy/dns-made-easy-connection-types";
|
||||||
import {
|
import {
|
||||||
TFlyioConnection,
|
TFlyioConnection,
|
||||||
TFlyioConnectionConfig,
|
TFlyioConnectionConfig,
|
||||||
@@ -279,6 +285,7 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TGitLabConnection
|
| TGitLabConnection
|
||||||
| TCloudflareConnection
|
| TCloudflareConnection
|
||||||
| TBitbucketConnection
|
| TBitbucketConnection
|
||||||
|
| TDNSMadeEasyConnection
|
||||||
| TZabbixConnection
|
| TZabbixConnection
|
||||||
| TRailwayConnection
|
| TRailwayConnection
|
||||||
| TChecklyConnection
|
| TChecklyConnection
|
||||||
@@ -328,6 +335,7 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TGitLabConnectionInput
|
| TGitLabConnectionInput
|
||||||
| TCloudflareConnectionInput
|
| TCloudflareConnectionInput
|
||||||
| TBitbucketConnectionInput
|
| TBitbucketConnectionInput
|
||||||
|
| TDNSMadeEasyConnectionInput
|
||||||
| TZabbixConnectionInput
|
| TZabbixConnectionInput
|
||||||
| TRailwayConnectionInput
|
| TRailwayConnectionInput
|
||||||
| TChecklyConnectionInput
|
| TChecklyConnectionInput
|
||||||
@@ -395,6 +403,7 @@ export type TAppConnectionConfig =
|
|||||||
| TGitLabConnectionConfig
|
| TGitLabConnectionConfig
|
||||||
| TCloudflareConnectionConfig
|
| TCloudflareConnectionConfig
|
||||||
| TBitbucketConnectionConfig
|
| TBitbucketConnectionConfig
|
||||||
|
| TDNSMadeEasyConnectionConfig
|
||||||
| TZabbixConnectionConfig
|
| TZabbixConnectionConfig
|
||||||
| TRailwayConnectionConfig
|
| TRailwayConnectionConfig
|
||||||
| TChecklyConnectionConfig
|
| TChecklyConnectionConfig
|
||||||
@@ -439,6 +448,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateGitLabConnectionCredentialsSchema
|
| TValidateGitLabConnectionCredentialsSchema
|
||||||
| TValidateCloudflareConnectionCredentialsSchema
|
| TValidateCloudflareConnectionCredentialsSchema
|
||||||
| TValidateBitbucketConnectionCredentialsSchema
|
| TValidateBitbucketConnectionCredentialsSchema
|
||||||
|
| TValidateDNSMadeEasyConnectionCredentialsSchema
|
||||||
| TValidateZabbixConnectionCredentialsSchema
|
| TValidateZabbixConnectionCredentialsSchema
|
||||||
| TValidateRailwayConnectionCredentialsSchema
|
| TValidateRailwayConnectionCredentialsSchema
|
||||||
| TValidateChecklyConnectionCredentialsSchema
|
| TValidateChecklyConnectionCredentialsSchema
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export enum DNSMadeEasyConnectionMethod {
|
||||||
|
APIKeySecret = "api-key-secret"
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
|
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
|
||||||
|
import {
|
||||||
|
TDNSMadeEasyConnection,
|
||||||
|
TDNSMadeEasyConnectionConfig,
|
||||||
|
TDNSMadeEasyZone
|
||||||
|
} from "./dns-made-easy-connection-types";
|
||||||
|
|
||||||
|
interface DNSMadeEasyApiResponse {
|
||||||
|
totalRecords: number;
|
||||||
|
totalPages: number;
|
||||||
|
data: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
page: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDNSMadeEasyUrl = (path: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
return `${appCfg.DNS_MADE_EASY_SANDBOX_ENABLED ? IntegrationUrls.DNS_MADE_EASY_SANDBOX_API_URL : IntegrationUrls.DNS_MADE_EASY_API_URL}${path}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeDNSMadeEasyAuthHeaders = (
|
||||||
|
apiKey: string,
|
||||||
|
secretKey: string,
|
||||||
|
currentDate?: Date
|
||||||
|
): Record<string, string> => {
|
||||||
|
// Format date as "Day, DD Mon YYYY HH:MM:SS GMT" (e.g., "Mon, 01 Jan 2024 12:00:00 GMT")
|
||||||
|
const requestDate = (currentDate ?? new Date()).toUTCString();
|
||||||
|
|
||||||
|
// Generate HMAC-SHA1 signature
|
||||||
|
const hmac = crypto.nativeCrypto.createHmac("sha1", secretKey);
|
||||||
|
hmac.update(requestDate);
|
||||||
|
const hmacSignature = hmac.digest("hex");
|
||||||
|
|
||||||
|
return {
|
||||||
|
"x-dnsme-apiKey": apiKey,
|
||||||
|
"x-dnsme-hmac": hmacSignature,
|
||||||
|
"x-dnsme-requestDate": requestDate
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDNSMadeEasyConnectionListItem = () => {
|
||||||
|
return {
|
||||||
|
name: "DNS Made Easy" as const,
|
||||||
|
app: AppConnection.DNSMadeEasy as const,
|
||||||
|
methods: Object.values(DNSMadeEasyConnectionMethod) as [DNSMadeEasyConnectionMethod.APIKeySecret]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listDNSMadeEasyZones = async (appConnection: TDNSMadeEasyConnection): Promise<TDNSMadeEasyZone[]> => {
|
||||||
|
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||||
|
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
credentials: { apiKey, secretKey }
|
||||||
|
} = appConnection;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allZones: TDNSMadeEasyZone[] = [];
|
||||||
|
let currentPage = 0;
|
||||||
|
let totalPages = 1;
|
||||||
|
|
||||||
|
// Fetch all pages of zones
|
||||||
|
while (currentPage < totalPages) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const resp = await request.get<DNSMadeEasyApiResponse>(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
|
||||||
|
headers: {
|
||||||
|
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||||
|
Accept: "application/json"
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
page: currentPage
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.data?.data) {
|
||||||
|
// Map the API response to TDNSMadeEasyZone format
|
||||||
|
const zones = resp.data.data.map((zone) => ({
|
||||||
|
id: String(zone.id),
|
||||||
|
name: zone.name
|
||||||
|
}));
|
||||||
|
allZones.push(...zones);
|
||||||
|
|
||||||
|
// Update pagination info
|
||||||
|
totalPages = resp.data.totalPages || 1;
|
||||||
|
currentPage += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allZones;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error(error, "Error listing DNS Made Easy zones");
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
message: `Failed to list DNS Made Easy zones: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to list DNS Made Easy zones"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listDNSMadeEasyRecords = async (
|
||||||
|
appConnection: TDNSMadeEasyConnection,
|
||||||
|
options: { zoneId: string; type?: string; name?: string }
|
||||||
|
): Promise<DNSMadeEasyApiResponse["data"]> => {
|
||||||
|
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||||
|
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
credentials: { apiKey, secretKey }
|
||||||
|
} = appConnection;
|
||||||
|
const { zoneId, type, name } = options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allRecords: DNSMadeEasyApiResponse["data"] = [];
|
||||||
|
let currentPage = 0;
|
||||||
|
let totalPages = 1;
|
||||||
|
|
||||||
|
// Fetch all pages of records
|
||||||
|
while (currentPage < totalPages) {
|
||||||
|
// Build query parameters
|
||||||
|
const queryParams: Record<string, string | number> = {};
|
||||||
|
if (type) {
|
||||||
|
queryParams.type = type;
|
||||||
|
}
|
||||||
|
if (name) {
|
||||||
|
queryParams.recordName = name;
|
||||||
|
}
|
||||||
|
queryParams.page = currentPage;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const resp = await request.get<DNSMadeEasyApiResponse>(
|
||||||
|
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(zoneId)}/records`),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||||
|
Accept: "application/json"
|
||||||
|
},
|
||||||
|
params: queryParams
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resp.data?.data) {
|
||||||
|
allRecords.push(...resp.data.data);
|
||||||
|
|
||||||
|
// Update pagination info
|
||||||
|
totalPages = resp.data.totalPages || 1;
|
||||||
|
currentPage += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allRecords;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error(error, "Error listing DNS Made Easy records");
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
message: `Failed to list DNS Made Easy records: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to list DNS Made Easy records"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateDNSMadeEasyConnectionCredentials = async (config: TDNSMadeEasyConnectionConfig) => {
|
||||||
|
if (config.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||||
|
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { apiKey, secretKey } = config.credentials;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await request.get(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
|
||||||
|
headers: {
|
||||||
|
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to validate connection: Invalid API credentials provided."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
message: `Failed to validate credentials: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
logger.error(error, "Error validating DNS Made Easy connection credentials");
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to validate connection: verify credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.credentials;
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseAppConnectionSchema,
|
||||||
|
GenericCreateAppConnectionFieldsSchema,
|
||||||
|
GenericUpdateAppConnectionFieldsSchema
|
||||||
|
} from "@app/services/app-connection/app-connection-schemas";
|
||||||
|
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
|
||||||
|
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
|
||||||
|
|
||||||
|
export const DNSMadeEasyConnectionApiKeyCredentialsSchema = z.object({
|
||||||
|
apiKey: z.string().trim().min(1, "API key required").max(256, "API key cannot exceed 256 characters"),
|
||||||
|
secretKey: z.string().trim().min(1, "Secret key required").max(256, "Secret key cannot exceed 256 characters")
|
||||||
|
});
|
||||||
|
|
||||||
|
const BaseDNSMadeEasyConnectionSchema = BaseAppConnectionSchema.extend({
|
||||||
|
app: z.literal(AppConnection.DNSMadeEasy)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DNSMadeEasyConnectionSchema = BaseDNSMadeEasyConnectionSchema.extend({
|
||||||
|
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||||
|
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedDNSMadeEasyConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseDNSMadeEasyConnectionSchema.extend({
|
||||||
|
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||||
|
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.pick({ apiKey: true })
|
||||||
|
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy]} (API Key)` }))
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ValidateDNSMadeEasyConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(DNSMadeEasyConnectionMethod.APIKeySecret)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.DNSMadeEasy).method),
|
||||||
|
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.DNSMadeEasy).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateDNSMadeEasyConnectionSchema = ValidateDNSMadeEasyConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateDNSMadeEasyConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.optional().describe(
|
||||||
|
AppConnections.UPDATE(AppConnection.DNSMadeEasy).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy));
|
||||||
|
|
||||||
|
export const DNSMadeEasyConnectionListItemSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.literal("DNS Made Easy"),
|
||||||
|
app: z.literal(AppConnection.DNSMadeEasy),
|
||||||
|
methods: z.nativeEnum(DNSMadeEasyConnectionMethod).array()
|
||||||
|
})
|
||||||
|
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy] }));
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { listDNSMadeEasyZones } from "./dns-made-easy-connection-fns";
|
||||||
|
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TDNSMadeEasyConnection>;
|
||||||
|
|
||||||
|
export const dnsMadeEasyConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||||
|
const listZones = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.DNSMadeEasy, connectionId, actor);
|
||||||
|
try {
|
||||||
|
const zones = await listDNSMadeEasyZones(appConnection);
|
||||||
|
return zones;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
error,
|
||||||
|
`Failed to list DNS Made Easy zones for DNS Made Easy connection [connectionId=${connectionId}]`
|
||||||
|
);
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to list DNS Made Easy zones: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listZones
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateDNSMadeEasyConnectionSchema,
|
||||||
|
DNSMadeEasyConnectionSchema,
|
||||||
|
ValidateDNSMadeEasyConnectionCredentialsSchema
|
||||||
|
} from "./dns-made-easy-connection-schema";
|
||||||
|
|
||||||
|
export type TDNSMadeEasyConnection = z.infer<typeof DNSMadeEasyConnectionSchema>;
|
||||||
|
|
||||||
|
export type TDNSMadeEasyConnectionInput = z.infer<typeof CreateDNSMadeEasyConnectionSchema> & {
|
||||||
|
app: AppConnection.DNSMadeEasy;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateDNSMadeEasyConnectionCredentialsSchema = typeof ValidateDNSMadeEasyConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TDNSMadeEasyConnectionConfig = DiscriminativePick<
|
||||||
|
TDNSMadeEasyConnectionInput,
|
||||||
|
"method" | "app" | "credentials"
|
||||||
|
> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDNSMadeEasyZone = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum AcmeDnsProvider {
|
export enum AcmeDnsProvider {
|
||||||
Route53 = "route53",
|
Route53 = "route53",
|
||||||
Cloudflare = "cloudflare"
|
Cloudflare = "cloudflare",
|
||||||
|
DNSMadeEasy = "dns-made-easy"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
|
|||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||||
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
|
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
|
||||||
|
import { TDNSMadeEasyConnection } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-types";
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
@@ -43,6 +44,7 @@ import {
|
|||||||
TUpdateAcmeCertificateAuthorityDTO
|
TUpdateAcmeCertificateAuthorityDTO
|
||||||
} from "./acme-certificate-authority-types";
|
} from "./acme-certificate-authority-types";
|
||||||
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
|
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
|
||||||
|
import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy";
|
||||||
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
||||||
|
|
||||||
type TAcmeCertificateAuthorityFnsDeps = {
|
type TAcmeCertificateAuthorityFnsDeps = {
|
||||||
@@ -120,6 +122,22 @@ export const castDbEntryToAcmeCertificateAuthority = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAcmeChallengeRecord = (
|
||||||
|
provider: AcmeDnsProvider,
|
||||||
|
identifierValue: string,
|
||||||
|
keyAuthorization: string
|
||||||
|
): { recordName: string; recordValue: string } => {
|
||||||
|
let recordName: string;
|
||||||
|
if (provider === AcmeDnsProvider.DNSMadeEasy) {
|
||||||
|
// For DNS Made Easy, we don't need to provide the domain name in the record name.
|
||||||
|
recordName = "_acme-challenge";
|
||||||
|
} else {
|
||||||
|
recordName = `_acme-challenge.${identifierValue}`; // e.g., "_acme-challenge.example.com"
|
||||||
|
}
|
||||||
|
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||||
|
return { recordName, recordValue };
|
||||||
|
};
|
||||||
|
|
||||||
export const orderCertificate = async (
|
export const orderCertificate = async (
|
||||||
{
|
{
|
||||||
caId,
|
caId,
|
||||||
@@ -241,8 +259,11 @@ export const orderCertificate = async (
|
|||||||
throw new Error("Unsupported challenge type");
|
throw new Error("Unsupported challenge type");
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
const { recordName, recordValue } = getAcmeChallengeRecord(
|
||||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
acmeCa.configuration.dnsProviderConfig.provider,
|
||||||
|
authz.identifier.value,
|
||||||
|
keyAuthorization
|
||||||
|
);
|
||||||
|
|
||||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||||
case AcmeDnsProvider.Route53: {
|
case AcmeDnsProvider.Route53: {
|
||||||
@@ -263,14 +284,26 @@ export const orderCertificate = async (
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case AcmeDnsProvider.DNSMadeEasy: {
|
||||||
|
await dnsMadeEasyInsertTxtRecord(
|
||||||
|
connection as TDNSMadeEasyConnection,
|
||||||
|
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||||
|
recordName,
|
||||||
|
recordValue
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
const { recordName, recordValue } = getAcmeChallengeRecord(
|
||||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
acmeCa.configuration.dnsProviderConfig.provider,
|
||||||
|
authz.identifier.value,
|
||||||
|
keyAuthorization
|
||||||
|
);
|
||||||
|
|
||||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||||
case AcmeDnsProvider.Route53: {
|
case AcmeDnsProvider.Route53: {
|
||||||
@@ -291,6 +324,15 @@ export const orderCertificate = async (
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case AcmeDnsProvider.DNSMadeEasy: {
|
||||||
|
await dnsMadeEasyDeleteTxtRecord(
|
||||||
|
connection as TDNSMadeEasyConnection,
|
||||||
|
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||||
|
recordName,
|
||||||
|
recordValue
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||||
}
|
}
|
||||||
@@ -413,6 +455,12 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dnsProviderConfig.provider === AcmeDnsProvider.DNSMadeEasy && appConnection.app !== AppConnection.DNSMadeEasy) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `App connection with ID '${dnsAppConnectionId}' is not a DNS Made Easy connection`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// validates permission to connect
|
// validates permission to connect
|
||||||
await appConnectionService.validateAppConnectionUsageById(
|
await appConnectionService.validateAppConnectionUsageById(
|
||||||
appConnection.app as AppConnection,
|
appConnection.app as AppConnection,
|
||||||
@@ -508,6 +556,15 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
dnsProviderConfig.provider === AcmeDnsProvider.DNSMadeEasy &&
|
||||||
|
appConnection.app !== AppConnection.DNSMadeEasy
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `App connection with ID '${dnsAppConnectionId}' is not a DNS Made Easy connection`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findById(id);
|
const ca = await certificateAuthorityDAL.findById(id);
|
||||||
|
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import {
|
||||||
|
getDNSMadeEasyUrl,
|
||||||
|
listDNSMadeEasyRecords,
|
||||||
|
makeDNSMadeEasyAuthHeaders
|
||||||
|
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-fns";
|
||||||
|
import { TDNSMadeEasyConnection } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-types";
|
||||||
|
|
||||||
|
export const dnsMadeEasyInsertTxtRecord = async (
|
||||||
|
connection: TDNSMadeEasyConnection,
|
||||||
|
hostedZoneId: string,
|
||||||
|
domain: string,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
credentials: { apiKey, secretKey }
|
||||||
|
} = connection;
|
||||||
|
|
||||||
|
logger.info({ hostedZoneId, domain, value }, "Inserting TXT record for DNS Made Easy");
|
||||||
|
try {
|
||||||
|
await request.post(
|
||||||
|
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(hostedZoneId)}/records`),
|
||||||
|
{
|
||||||
|
type: "TXT",
|
||||||
|
name: domain,
|
||||||
|
value,
|
||||||
|
ttl: 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const errorMessage =
|
||||||
|
(error.response?.data as { error?: string[] | string })?.error?.[0] ||
|
||||||
|
(error.response?.data as { error?: string[] | string })?.error ||
|
||||||
|
error.message ||
|
||||||
|
"Unknown error";
|
||||||
|
|
||||||
|
if (error.status === 400 && error.message.includes("already exists")) {
|
||||||
|
logger.info({ domain, value }, `Record already exists for domain: ${domain} and value: ${value}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(typeof errorMessage === "string" ? errorMessage : String(errorMessage));
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dnsMadeEasyDeleteTxtRecord = async (
|
||||||
|
connection: TDNSMadeEasyConnection,
|
||||||
|
hostedZoneId: string,
|
||||||
|
domain: string,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
credentials: { apiKey, secretKey }
|
||||||
|
} = connection;
|
||||||
|
|
||||||
|
logger.info({ hostedZoneId, domain, value }, "Deleting TXT record for DNS Made Easy");
|
||||||
|
try {
|
||||||
|
const dnsRecords = await listDNSMadeEasyRecords(connection, { zoneId: hostedZoneId, type: "TXT", name: domain });
|
||||||
|
|
||||||
|
let foundRecord = false;
|
||||||
|
if (dnsRecords.length > 0) {
|
||||||
|
const recordToDelete = dnsRecords.find(
|
||||||
|
(record) => record.type === "TXT" && record.name === domain && record.value === value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recordToDelete) {
|
||||||
|
await request.delete(
|
||||||
|
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(hostedZoneId)}/records/${recordToDelete.id}`),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
foundRecord = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundRecord) {
|
||||||
|
logger.warn({ hostedZoneId, domain, value }, "Record to delete not found");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const errorMessage =
|
||||||
|
(error.response?.data as { error?: string[] | string })?.error?.[0] ||
|
||||||
|
(error.response?.data as { error?: string[] | string })?.error ||
|
||||||
|
error.message ||
|
||||||
|
"Unknown error";
|
||||||
|
throw new Error(typeof errorMessage === "string" ? errorMessage : String(errorMessage));
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -67,6 +67,12 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for EST enrollment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
||||||
if (!estConfig) {
|
if (!estConfig) {
|
||||||
throw new NotFoundError({ message: "EST configuration not found" });
|
throw new NotFoundError({ message: "EST configuration not found" });
|
||||||
@@ -169,6 +175,12 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for EST enrollment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
||||||
if (!estConfig) {
|
if (!estConfig) {
|
||||||
throw new NotFoundError({ message: "EST configuration not found" });
|
throw new NotFoundError({ message: "EST configuration not found" });
|
||||||
@@ -281,6 +293,12 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for EST enrollment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
|
||||||
if (!estConfig) {
|
if (!estConfig) {
|
||||||
throw new NotFoundError({ message: "EST configuration not found" });
|
throw new NotFoundError({ message: "EST configuration not found" });
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ormify, selectAllTableCols } from "@app/lib/knex";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
EnrollmentType,
|
EnrollmentType,
|
||||||
|
IssuerType,
|
||||||
TCertificateProfile,
|
TCertificateProfile,
|
||||||
TCertificateProfileCertificate,
|
TCertificateProfileCertificate,
|
||||||
TCertificateProfileInsert,
|
TCertificateProfileInsert,
|
||||||
@@ -198,6 +199,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
slug: result.slug,
|
slug: result.slug,
|
||||||
description: result.description,
|
description: result.description,
|
||||||
enrollmentType: result.enrollmentType as EnrollmentType,
|
enrollmentType: result.enrollmentType as EnrollmentType,
|
||||||
|
issuerType: result.issuerType as IssuerType,
|
||||||
estConfigId: result.estConfigId,
|
estConfigId: result.estConfigId,
|
||||||
apiConfigId: result.apiConfigId,
|
apiConfigId: result.apiConfigId,
|
||||||
acmeConfigId: result.acmeConfigId,
|
acmeConfigId: result.acmeConfigId,
|
||||||
@@ -239,12 +241,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
} = {},
|
} = {},
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
|
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
|
||||||
try {
|
try {
|
||||||
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
|
const { offset = 0, limit = 20, search, enrollmentType, issuerType, caId } = options;
|
||||||
|
|
||||||
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
||||||
`${TableName.PkiCertificateProfile}.projectId`,
|
`${TableName.PkiCertificateProfile}.projectId`,
|
||||||
@@ -269,6 +272,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (issuerType) {
|
||||||
|
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.issuerType`, issuerType);
|
||||||
|
}
|
||||||
|
|
||||||
const query = baseQuery
|
const query = baseQuery
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.PkiEstEnrollmentConfig,
|
TableName.PkiEstEnrollmentConfig,
|
||||||
@@ -338,8 +345,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
slug: result.slug,
|
slug: result.slug,
|
||||||
description: result.description,
|
description: result.description,
|
||||||
enrollmentType: result.enrollmentType as EnrollmentType,
|
enrollmentType: result.enrollmentType as EnrollmentType,
|
||||||
|
issuerType: result.issuerType as IssuerType,
|
||||||
estConfigId: result.estConfigId,
|
estConfigId: result.estConfigId,
|
||||||
apiConfigId: result.apiConfigId,
|
apiConfigId: result.apiConfigId,
|
||||||
|
acmeConfigId: result.acmeConfigId,
|
||||||
createdAt: result.createdAt,
|
createdAt: result.createdAt,
|
||||||
updatedAt: result.updatedAt,
|
updatedAt: result.updatedAt,
|
||||||
estConfig,
|
estConfig,
|
||||||
@@ -359,12 +368,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
options: {
|
options: {
|
||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
} = {},
|
} = {},
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<number> => {
|
): Promise<number> => {
|
||||||
try {
|
try {
|
||||||
const { search, enrollmentType, caId } = options;
|
const { search, enrollmentType, issuerType, caId } = options;
|
||||||
|
|
||||||
let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId });
|
let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId });
|
||||||
|
|
||||||
@@ -384,6 +394,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
query = query.where({ caId });
|
query = query.where({ caId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (issuerType) {
|
||||||
|
query = query.where({ issuerType });
|
||||||
|
}
|
||||||
|
|
||||||
const result = await query.count("*").first();
|
const result = await query.count("*").first();
|
||||||
return parseInt((result as unknown as { count: string }).count || "0", 10);
|
return parseInt((result as unknown as { count: string }).count || "0", 10);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EnrollmentType } from "./certificate-profile-types";
|
import { CertStatus } from "../certificate/certificate-types";
|
||||||
|
import { EnrollmentType, IssuerType } from "./certificate-profile-types";
|
||||||
|
|
||||||
export const createCertificateProfileSchema = z
|
export const createCertificateProfileSchema = z
|
||||||
.object({
|
.object({
|
||||||
projectId: z.string().uuid("Project ID must be valid"),
|
projectId: z.string().uuid("Project ID must be valid"),
|
||||||
caId: z.string().uuid(),
|
caId: z.string().uuid().nullable().optional(),
|
||||||
certificateTemplateId: z.string().uuid(),
|
certificateTemplateId: z.string().uuid(),
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
@@ -15,6 +16,7 @@ export const createCertificateProfileSchema = z
|
|||||||
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType),
|
enrollmentType: z.nativeEnum(EnrollmentType),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).default(IssuerType.CA),
|
||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
@@ -33,43 +35,100 @@ export const createCertificateProfileSchema = z
|
|||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.enrollmentType === EnrollmentType.EST) {
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
if (!data.estConfig) {
|
return !!data.estConfig;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === EnrollmentType.API) {
|
|
||||||
if (!data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.estConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === EnrollmentType.ACME) {
|
|
||||||
if (!data.acmeConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.estConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (data.apiConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
message: "EST enrollment type requires EST configuration"
|
||||||
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration."
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !!data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type requires API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !!data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type requires ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
|
return !data.apiConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "EST enrollment type cannot have API or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !data.estConfig && !data.acmeConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type cannot have EST or ACME configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.ACME) {
|
||||||
|
return !data.estConfig && !data.apiConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "ACME enrollment type cannot have EST or API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.CA) {
|
||||||
|
return !!data.caId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "CA issuer type requires a CA ID"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return !data.caId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type cannot have a CA ID"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return data.enrollmentType === EnrollmentType.API;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type only supports API enrollment"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -83,6 +142,7 @@ export const updateCertificateProfileSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).optional(),
|
||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
@@ -100,19 +160,34 @@ export const updateCertificateProfileSchema = z
|
|||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.enrollmentType === EnrollmentType.EST) {
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
if (data.apiConfig) {
|
return !data.apiConfig;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.enrollmentType === EnrollmentType.API) {
|
|
||||||
if (data.estConfig) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Cannot have EST config with API enrollment type or API config with EST enrollment type."
|
message: "EST enrollment type cannot have API configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
return !data.estConfig;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "API enrollment type cannot have EST configuration"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
return !data.enrollmentType || data.enrollmentType === EnrollmentType.API;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Self-signed issuer type only supports API enrollment"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -131,6 +206,7 @@ export const listCertificateProfilesSchema = z.object({
|
|||||||
limit: z.coerce.number().min(1).max(100).default(20),
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
|
issuerType: z.nativeEnum(IssuerType).optional(),
|
||||||
caId: z.string().uuid().optional()
|
caId: z.string().uuid().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,6 +218,6 @@ export const listCertificatesByProfileSchema = z.object({
|
|||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
offset: z.coerce.number().min(0).default(0),
|
offset: z.coerce.number().min(0).default(0),
|
||||||
limit: z.coerce.number().min(1).max(100).default(20),
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
status: z.enum(["active", "expired", "revoked"]).optional(),
|
status: z.nativeEnum(CertStatus).optional(),
|
||||||
search: z.string().optional()
|
search: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ import type { TKmsServiceFactory } from "../kms/kms-service";
|
|||||||
import type { TProjectDALFactory } from "../project/project-dal";
|
import type { TProjectDALFactory } from "../project/project-dal";
|
||||||
import type { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
import type { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
||||||
import { certificateProfileServiceFactory, TCertificateProfileServiceFactory } from "./certificate-profile-service";
|
import { certificateProfileServiceFactory, TCertificateProfileServiceFactory } from "./certificate-profile-service";
|
||||||
import { EnrollmentType, TCertificateProfile, TCertificateProfileWithConfigs } from "./certificate-profile-types";
|
import {
|
||||||
|
EnrollmentType,
|
||||||
|
IssuerType,
|
||||||
|
TCertificateProfile,
|
||||||
|
TCertificateProfileWithConfigs
|
||||||
|
} from "./certificate-profile-types";
|
||||||
|
|
||||||
vi.mock("@app/lib/crypto/cryptography", () => ({
|
vi.mock("@app/lib/crypto/cryptography", () => ({
|
||||||
crypto: {
|
crypto: {
|
||||||
@@ -90,6 +95,7 @@ describe("CertificateProfileService", () => {
|
|||||||
description: "Test certificate profile",
|
description: "Test certificate profile",
|
||||||
slug: "test-profile",
|
slug: "test-profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfigId: "api-config-123",
|
apiConfigId: "api-config-123",
|
||||||
@@ -272,6 +278,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "new-profile",
|
slug: "new-profile",
|
||||||
description: "New test profile",
|
description: "New test profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -312,6 +319,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "new-profile",
|
slug: "new-profile",
|
||||||
description: "New test profile",
|
description: "New test profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfigId: "api-config-123",
|
apiConfigId: "api-config-123",
|
||||||
@@ -383,6 +391,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "invalid-profile",
|
slug: "invalid-profile",
|
||||||
description: "Invalid test profile",
|
description: "Invalid test profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123"
|
certificateTemplateId: "template-123"
|
||||||
};
|
};
|
||||||
@@ -401,6 +410,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "api-profile",
|
slug: "api-profile",
|
||||||
description: "Profile with API enrollment",
|
description: "Profile with API enrollment",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -726,6 +736,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "est-profile",
|
slug: "est-profile",
|
||||||
description: "Profile with EST enrollment",
|
description: "Profile with EST enrollment",
|
||||||
enrollmentType: EnrollmentType.EST,
|
enrollmentType: EnrollmentType.EST,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
estConfig: {
|
estConfig: {
|
||||||
@@ -776,6 +787,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "different-profile-name",
|
slug: "different-profile-name",
|
||||||
description: "Profile with duplicate slug",
|
description: "Profile with duplicate slug",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -801,6 +813,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "auto-renew-profile",
|
slug: "auto-renew-profile",
|
||||||
description: "Profile with auto-renewal",
|
description: "Profile with auto-renewal",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -965,6 +978,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "invalid-template-profile",
|
slug: "invalid-template-profile",
|
||||||
description: "Profile with invalid template",
|
description: "Profile with invalid template",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "nonexistent-template",
|
certificateTemplateId: "nonexistent-template",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -990,6 +1004,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "concurrent-profile",
|
slug: "concurrent-profile",
|
||||||
description: "Profile created concurrently",
|
description: "Profile created concurrently",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -1018,6 +1033,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "cross-project-profile",
|
slug: "cross-project-profile",
|
||||||
description: "Profile using template from different project",
|
description: "Profile using template from different project",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-456",
|
certificateTemplateId: "template-456",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -1047,6 +1063,7 @@ describe("CertificateProfileService", () => {
|
|||||||
slug: "invalid-slug-profile",
|
slug: "invalid-slug-profile",
|
||||||
description: "Profile with invalid slug format",
|
description: "Profile with invalid slug format",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
|||||||
import { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
||||||
import {
|
import {
|
||||||
EnrollmentType,
|
EnrollmentType,
|
||||||
|
IssuerType,
|
||||||
TCertificateProfile,
|
TCertificateProfile,
|
||||||
TCertificateProfileCertificate,
|
TCertificateProfileCertificate,
|
||||||
TCertificateProfileInsert,
|
TCertificateProfileInsert,
|
||||||
@@ -39,6 +40,34 @@ import {
|
|||||||
TCertificateProfileWithConfigs
|
TCertificateProfileWithConfigs
|
||||||
} from "./certificate-profile-types";
|
} from "./certificate-profile-types";
|
||||||
|
|
||||||
|
const validateIssuerTypeConstraints = (
|
||||||
|
issuerType: IssuerType,
|
||||||
|
enrollmentType: EnrollmentType,
|
||||||
|
caId: string | null,
|
||||||
|
existingCaId?: string | null
|
||||||
|
) => {
|
||||||
|
if (issuerType === IssuerType.CA) {
|
||||||
|
if (!caId && !existingCaId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "CA issuer type requires a Certificate Authority to be selected"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
if (caId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Self-signed issuer type cannot have a Certificate Authority"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (enrollmentType !== EnrollmentType.API) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Self-signed issuer type only supports API enrollment"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const generateAndEncryptAcmeEabSecret = async (
|
const generateAndEncryptAcmeEabSecret = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey">,
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey">,
|
||||||
@@ -163,7 +192,8 @@ export type TCertificateProfileServiceFactory = ReturnType<typeof certificatePro
|
|||||||
const convertDalToService = (dalResult: Record<string, unknown>): TCertificateProfile => {
|
const convertDalToService = (dalResult: Record<string, unknown>): TCertificateProfile => {
|
||||||
return {
|
return {
|
||||||
...dalResult,
|
...dalResult,
|
||||||
enrollmentType: dalResult.enrollmentType as EnrollmentType
|
enrollmentType: dalResult.enrollmentType as EnrollmentType,
|
||||||
|
issuerType: dalResult.issuerType as IssuerType
|
||||||
} as TCertificateProfile;
|
} as TCertificateProfile;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,6 +270,8 @@ export const certificateProfileServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateIssuerTypeConstraints(data.issuerType, data.enrollmentType, data.caId ?? null);
|
||||||
|
|
||||||
// Validate enrollment configuration requirements
|
// Validate enrollment configuration requirements
|
||||||
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
|
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
@@ -376,7 +408,16 @@ export const certificateProfileServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { estConfig, apiConfig, ...profileUpdateData } = data;
|
const finalIssuerType = data.issuerType || existingProfile.issuerType;
|
||||||
|
const finalEnrollmentType = data.enrollmentType || existingProfile.enrollmentType;
|
||||||
|
const finalCaId = data.caId !== undefined ? data.caId : existingProfile.caId;
|
||||||
|
|
||||||
|
validateIssuerTypeConstraints(finalIssuerType, finalEnrollmentType, finalCaId ?? null, existingProfile.caId);
|
||||||
|
|
||||||
|
const updatedData =
|
||||||
|
finalIssuerType === IssuerType.SELF_SIGNED && existingProfile.caId ? { ...data, caId: null } : data;
|
||||||
|
|
||||||
|
const { estConfig, apiConfig, ...profileUpdateData } = updatedData;
|
||||||
|
|
||||||
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
|
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
|
||||||
if (estConfig && existingProfile.estConfigId) {
|
if (estConfig && existingProfile.estConfigId) {
|
||||||
@@ -569,6 +610,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit = 20,
|
limit = 20,
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
|
issuerType,
|
||||||
caId
|
caId
|
||||||
}: {
|
}: {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
@@ -580,6 +622,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
profiles: TCertificateProfileWithConfigs[];
|
profiles: TCertificateProfileWithConfigs[];
|
||||||
@@ -603,12 +646,14 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit,
|
limit,
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
|
issuerType,
|
||||||
caId
|
caId
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
|
issuerType,
|
||||||
caId
|
caId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,24 @@ export enum EnrollmentType {
|
|||||||
ACME = "acme"
|
ACME = "acme"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCertificateProfile = Omit<TPkiCertificateProfiles, "enrollmentType"> & {
|
export enum IssuerType {
|
||||||
|
CA = "ca",
|
||||||
|
SELF_SIGNED = "self-signed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCertificateProfile = Omit<TPkiCertificateProfiles, "enrollmentType" | "issuerType"> & {
|
||||||
enrollmentType: EnrollmentType;
|
enrollmentType: EnrollmentType;
|
||||||
|
issuerType: IssuerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType"> & {
|
export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType" | "issuerType"> & {
|
||||||
enrollmentType: EnrollmentType;
|
enrollmentType: EnrollmentType;
|
||||||
|
issuerType: IssuerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType"> & {
|
export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType" | "issuerType"> & {
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
CertSubjectAttributeType
|
CertSubjectAttributeType
|
||||||
} from "@app/services/certificate-common/certificate-constants";
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
|
||||||
import { ActorType, AuthMethod } from "../auth/auth-type";
|
import { ActorType, AuthMethod } from "../auth/auth-type";
|
||||||
@@ -40,18 +40,29 @@ vi.mock("../certificate-common/certificate-csr-utils", () => ({
|
|||||||
describe("CertificateV3Service", () => {
|
describe("CertificateV3Service", () => {
|
||||||
let service: TCertificateV3ServiceFactory;
|
let service: TCertificateV3ServiceFactory;
|
||||||
|
|
||||||
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction"> = {
|
const mockCertificateDAL: Pick<
|
||||||
|
TCertificateDALFactory,
|
||||||
|
"findOne" | "findById" | "updateById" | "transaction" | "create"
|
||||||
|
> = {
|
||||||
findOne: vi.fn(),
|
findOne: vi.fn(),
|
||||||
findById: vi.fn(),
|
findById: vi.fn(),
|
||||||
updateById: vi.fn(),
|
updateById: vi.fn(),
|
||||||
|
create: vi.fn().mockResolvedValue({
|
||||||
|
id: "new-cert-id",
|
||||||
|
serialNumber: "123456789",
|
||||||
|
friendlyName: "Test Certificate",
|
||||||
|
commonName: "test.example.com",
|
||||||
|
status: "ACTIVE"
|
||||||
|
}),
|
||||||
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
const mockTx = {};
|
const mockTx = {};
|
||||||
return callback(mockTx);
|
return callback(mockTx);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne"> = {
|
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create"> = {
|
||||||
findOne: vi.fn()
|
findOne: vi.fn(),
|
||||||
|
create: vi.fn()
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
|
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
|
||||||
@@ -150,7 +161,24 @@ describe("CertificateV3Service", () => {
|
|||||||
},
|
},
|
||||||
pkiSyncQueue: {
|
pkiSyncQueue: {
|
||||||
queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined)
|
queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined)
|
||||||
}
|
},
|
||||||
|
certificateBodyDAL: {
|
||||||
|
create: vi.fn().mockResolvedValue({ id: "body-123" })
|
||||||
|
},
|
||||||
|
kmsService: {
|
||||||
|
generateKmsKey: vi.fn().mockResolvedValue("kms-key-123"),
|
||||||
|
encryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("encrypted"))),
|
||||||
|
decryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("decrypted")))
|
||||||
|
},
|
||||||
|
projectDAL: {
|
||||||
|
findOne: vi.fn().mockResolvedValue({ id: "project-123" }),
|
||||||
|
findById: vi.fn().mockResolvedValue({ id: "project-123" }),
|
||||||
|
updateById: vi.fn().mockResolvedValue({ id: "project-123" }),
|
||||||
|
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
})
|
||||||
|
} as any
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,6 +203,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -319,6 +348,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -508,6 +538,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -561,6 +592,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -721,6 +753,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -772,6 +805,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -933,6 +967,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: profileId,
|
id: profileId,
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -971,6 +1006,7 @@ describe("CertificateV3Service", () => {
|
|||||||
caId: "ca-1",
|
caId: "ca-1",
|
||||||
certificateTemplateId: "template-1",
|
certificateTemplateId: "template-1",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
description: "Test profile for algorithm compatibility",
|
description: "Test profile for algorithm compatibility",
|
||||||
@@ -1552,6 +1588,7 @@ describe("CertificateV3Service", () => {
|
|||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
@@ -1733,9 +1770,9 @@ describe("CertificateV3Service", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject renewal if certificate is not from a profile", async () => {
|
it("should reject renewal if certificate has no profile and no CA", async () => {
|
||||||
const certWithoutProfile = { ...mockOriginalCert, profileId: null };
|
const certWithoutProfileAndCA = { ...mockOriginalCert, profileId: null, caId: null };
|
||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfile);
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfileAndCA);
|
||||||
|
|
||||||
// Set up transaction mock to properly handle errors
|
// Set up transaction mock to properly handle errors
|
||||||
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
@@ -2008,6 +2045,7 @@ describe("CertificateV3Service", () => {
|
|||||||
const mockProfile = {
|
const mockProfile = {
|
||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
projectId: "project-123"
|
projectId: "project-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2084,6 +2122,7 @@ describe("CertificateV3Service", () => {
|
|||||||
const mockProfile = {
|
const mockProfile = {
|
||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
projectId: "project-123"
|
projectId: "project-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2129,6 +2168,7 @@ describe("CertificateV3Service", () => {
|
|||||||
const mockProfile = {
|
const mockProfile = {
|
||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
projectId: "project-123"
|
projectId: "project-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2172,6 +2212,7 @@ describe("CertificateV3Service", () => {
|
|||||||
const mockProfile = {
|
const mockProfile = {
|
||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
|
issuerType: IssuerType.CA,
|
||||||
projectId: "project-123"
|
projectId: "project-123"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType, TCertificates } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
@@ -10,8 +11,11 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal";
|
import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal";
|
||||||
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
import {
|
import {
|
||||||
@@ -28,12 +32,25 @@ import {
|
|||||||
TCertificateAuthorityWithAssociatedCa
|
TCertificateAuthorityWithAssociatedCa
|
||||||
} from "@app/services/certificate-authority/certificate-authority-dal";
|
} from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
|
import {
|
||||||
|
createDistinguishedName,
|
||||||
|
createSerialNumber,
|
||||||
|
keyAlgorithmToAlgCfg,
|
||||||
|
signatureAlgorithmToAlgCfg
|
||||||
|
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants";
|
import {
|
||||||
|
CertExtendedKeyUsageType,
|
||||||
|
CertKeyUsageType,
|
||||||
|
CertSubjectAlternativeNameType
|
||||||
|
} from "../certificate-common/certificate-constants";
|
||||||
import {
|
import {
|
||||||
extractAlgorithmsFromCSR,
|
extractAlgorithmsFromCSR,
|
||||||
extractCertificateRequestFromCSR
|
extractCertificateRequestFromCSR
|
||||||
@@ -68,8 +85,9 @@ import {
|
|||||||
} from "./certificate-v3-types";
|
} from "./certificate-v3-types";
|
||||||
|
|
||||||
type TCertificateV3ServiceFactoryDep = {
|
type TCertificateV3ServiceFactoryDep = {
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction">;
|
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction" | "create">;
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
||||||
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById">;
|
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById">;
|
||||||
@@ -85,6 +103,8 @@ type TCertificateV3ServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||||
|
projectDAL: TProjectDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
||||||
@@ -329,6 +349,158 @@ const parseTtlToDays = (ttl: string): number => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateSelfSignedCertificate = async ({
|
||||||
|
certificateRequest,
|
||||||
|
template,
|
||||||
|
effectiveSignatureAlgorithm,
|
||||||
|
effectiveKeyAlgorithm
|
||||||
|
}: {
|
||||||
|
certificateRequest: {
|
||||||
|
commonName?: string;
|
||||||
|
keyUsages?: CertKeyUsageType[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||||
|
altNames?: Array<{
|
||||||
|
type: CertSubjectAlternativeNameType;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
validity: { ttl: string };
|
||||||
|
notBefore?: Date;
|
||||||
|
notAfter?: Date;
|
||||||
|
};
|
||||||
|
template?: {
|
||||||
|
subject?: Array<{
|
||||||
|
type: string;
|
||||||
|
allowed?: string[];
|
||||||
|
required?: string[];
|
||||||
|
denied?: string[];
|
||||||
|
}>;
|
||||||
|
sans?: Array<{
|
||||||
|
type: string;
|
||||||
|
allowed?: string[];
|
||||||
|
required?: string[];
|
||||||
|
denied?: string[];
|
||||||
|
}>;
|
||||||
|
} | null;
|
||||||
|
effectiveSignatureAlgorithm: CertSignatureAlgorithm;
|
||||||
|
effectiveKeyAlgorithm: CertKeyAlgorithm;
|
||||||
|
}): Promise<{
|
||||||
|
certificate: Buffer;
|
||||||
|
privateKey: Buffer;
|
||||||
|
serialNumber: string;
|
||||||
|
notBefore: Date;
|
||||||
|
notAfter: Date;
|
||||||
|
certificateSubject: Record<string, unknown>;
|
||||||
|
subjectAlternativeNames: Array<{
|
||||||
|
type: CertSubjectAlternativeNameType;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
}> => {
|
||||||
|
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template?.subject);
|
||||||
|
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
|
||||||
|
{ subjectAlternativeNames: certificateRequest.altNames },
|
||||||
|
template?.sans
|
||||||
|
);
|
||||||
|
|
||||||
|
const keyGenAlg = keyAlgorithmToAlgCfg(effectiveKeyAlgorithm);
|
||||||
|
const keyPair = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
|
const signatureAlgorithmConfig = signatureAlgorithmToAlgCfg(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
|
||||||
|
|
||||||
|
const notBeforeDate = certificateRequest.notBefore ? new Date(certificateRequest.notBefore) : new Date();
|
||||||
|
|
||||||
|
let notAfterDate: Date;
|
||||||
|
if (certificateRequest.notAfter) {
|
||||||
|
notAfterDate = new Date(certificateRequest.notAfter);
|
||||||
|
} else if (certificateRequest.validity.ttl) {
|
||||||
|
notAfterDate = new Date(new Date().getTime() + ms(certificateRequest.validity.ttl));
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Either notAfter date or TTL must be provided for certificate validity"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialNumber = createSerialNumber();
|
||||||
|
const dn = createDistinguishedName({
|
||||||
|
commonName: certificateSubject.common_name,
|
||||||
|
organization: certificateSubject.organization,
|
||||||
|
ou: certificateSubject.organizational_unit,
|
||||||
|
country: certificateSubject.country,
|
||||||
|
province: certificateSubject.state_or_province_name,
|
||||||
|
locality: certificateSubject.locality_name
|
||||||
|
});
|
||||||
|
|
||||||
|
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||||
|
name: dn,
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingAlgorithm: signatureAlgorithmConfig,
|
||||||
|
keys: keyPair,
|
||||||
|
extensions: [
|
||||||
|
new x509.BasicConstraintsExtension(false, undefined, false),
|
||||||
|
...(certificateRequest.keyUsages?.length
|
||||||
|
? [
|
||||||
|
new x509.KeyUsagesExtension(
|
||||||
|
(convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || []).reduce(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
(acc: number, usage) => acc | x509.KeyUsageFlags[usage],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(certificateRequest.extendedKeyUsages?.length
|
||||||
|
? [
|
||||||
|
new x509.ExtendedKeyUsageExtension(
|
||||||
|
(convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || []).map(
|
||||||
|
(eku) => x509.ExtendedKeyUsage[eku]
|
||||||
|
),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(subjectAlternativeNames
|
||||||
|
? [
|
||||||
|
new x509.SubjectAlternativeNameExtension(
|
||||||
|
certificateRequest.altNames?.map((san) => {
|
||||||
|
switch (san.type) {
|
||||||
|
case CertSubjectAlternativeNameType.DNS_NAME:
|
||||||
|
return { type: "dns" as const, value: san.value };
|
||||||
|
case CertSubjectAlternativeNameType.IP_ADDRESS:
|
||||||
|
return { type: "ip" as const, value: san.value };
|
||||||
|
case CertSubjectAlternativeNameType.EMAIL:
|
||||||
|
return { type: "email" as const, value: san.value };
|
||||||
|
case CertSubjectAlternativeNameType.URI:
|
||||||
|
return { type: "url" as const, value: san.value };
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}) || [],
|
||||||
|
false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificatePem = cert.toString("pem");
|
||||||
|
const privateKeyObj = crypto.nativeCrypto.KeyObject.from(keyPair.privateKey);
|
||||||
|
const privateKeyPem = privateKeyObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: Buffer.from(certificatePem),
|
||||||
|
privateKey: Buffer.from(privateKeyPem),
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
certificateSubject,
|
||||||
|
subjectAlternativeNames: certificateRequest.altNames || []
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const calculateFinalRenewBeforeDays = (
|
const calculateFinalRenewBeforeDays = (
|
||||||
profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } },
|
profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } },
|
||||||
ttl: string,
|
ttl: string,
|
||||||
@@ -348,8 +520,248 @@ const calculateFinalRenewBeforeDays = (
|
|||||||
return isValidRenewalTiming(renewBeforeDays, certificateExpiryDate) ? renewBeforeDays : undefined;
|
return isValidRenewalTiming(renewBeforeDays, certificateExpiryDate) ? renewBeforeDays : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEffectiveAlgorithms = (
|
||||||
|
requestSignatureAlgorithm?: CertSignatureAlgorithm,
|
||||||
|
requestKeyAlgorithm?: CertKeyAlgorithm,
|
||||||
|
originalSignatureAlgorithm?: CertSignatureAlgorithm,
|
||||||
|
originalKeyAlgorithm?: CertKeyAlgorithm
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
signatureAlgorithm: requestSignatureAlgorithm || originalSignatureAlgorithm || CertSignatureAlgorithm.RSA_SHA256,
|
||||||
|
keyAlgorithm: requestKeyAlgorithm || originalKeyAlgorithm || CertKeyAlgorithm.RSA_2048
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSelfSignedCertificateRecord = async ({
|
||||||
|
selfSignedResult,
|
||||||
|
certificateRequest,
|
||||||
|
profile,
|
||||||
|
originalCert,
|
||||||
|
certificateDAL,
|
||||||
|
tx,
|
||||||
|
isRenewal = false
|
||||||
|
}: {
|
||||||
|
selfSignedResult: Awaited<ReturnType<typeof generateSelfSignedCertificate>>;
|
||||||
|
certificateRequest: {
|
||||||
|
commonName?: string;
|
||||||
|
keyUsages?: CertKeyUsageType[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||||
|
};
|
||||||
|
profile?: { id: string; projectId: string } | null;
|
||||||
|
originalCert?: {
|
||||||
|
id: string;
|
||||||
|
friendlyName?: string | null;
|
||||||
|
commonName?: string | null;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "updateById">;
|
||||||
|
tx: Parameters<TCertificateDALFactory["create"]>[1];
|
||||||
|
isRenewal?: boolean;
|
||||||
|
}) => {
|
||||||
|
const subjectCommonName =
|
||||||
|
(selfSignedResult.certificateSubject.common_name as string) ||
|
||||||
|
certificateRequest.commonName ||
|
||||||
|
originalCert?.commonName ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
const altNamesList = selfSignedResult.subjectAlternativeNames.map((san) => san.value).join(",");
|
||||||
|
|
||||||
|
const projectId = originalCert?.projectId || profile?.projectId;
|
||||||
|
if (!projectId) {
|
||||||
|
throw new BadRequestError({ message: "Project ID is required for certificate creation" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseRecord = {
|
||||||
|
serialNumber: selfSignedResult.serialNumber,
|
||||||
|
friendlyName: originalCert?.friendlyName || subjectCommonName,
|
||||||
|
commonName: subjectCommonName,
|
||||||
|
altNames: altNamesList,
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
notBefore: selfSignedResult.notBefore,
|
||||||
|
notAfter: selfSignedResult.notAfter,
|
||||||
|
projectId,
|
||||||
|
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || [],
|
||||||
|
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || [],
|
||||||
|
profileId: profile?.id || null
|
||||||
|
};
|
||||||
|
|
||||||
|
const renewalRecord =
|
||||||
|
isRenewal && originalCert
|
||||||
|
? {
|
||||||
|
renewedFromCertificateId: originalCert.id
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return certificateDAL.create(
|
||||||
|
{
|
||||||
|
...baseRecord,
|
||||||
|
...renewalRecord
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createEncryptedCertificateData = async ({
|
||||||
|
certificateId,
|
||||||
|
certificate,
|
||||||
|
privateKey,
|
||||||
|
projectId,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
tx
|
||||||
|
}: {
|
||||||
|
certificateId: string;
|
||||||
|
certificate: Buffer;
|
||||||
|
privateKey: Buffer;
|
||||||
|
projectId: string;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "encryptWithKmsKey" | "generateKmsKey">;
|
||||||
|
projectDAL: TProjectDALFactory;
|
||||||
|
tx: Parameters<TCertificateBodyDALFactory["create"]>[1];
|
||||||
|
}) => {
|
||||||
|
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({ kmsId: certificateManagerKeyId });
|
||||||
|
|
||||||
|
const encryptedCertificate = await kmsEncryptor({
|
||||||
|
plainText: certificate
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
certId: certificateId,
|
||||||
|
encryptedCertificate: encryptedCertificate.cipherTextBlob
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const encryptedPrivateKey = await kmsEncryptor({
|
||||||
|
plainText: privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateSecretDAL.create(
|
||||||
|
{
|
||||||
|
certId: certificateId,
|
||||||
|
encryptedPrivateKey: encryptedPrivateKey.cipherTextBlob
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processSelfSignedCertificate = async ({
|
||||||
|
certificateRequest,
|
||||||
|
template,
|
||||||
|
profile,
|
||||||
|
originalCert,
|
||||||
|
effectiveAlgorithms,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
tx,
|
||||||
|
isRenewal = false
|
||||||
|
}: {
|
||||||
|
certificateRequest: {
|
||||||
|
commonName?: string;
|
||||||
|
keyUsages?: CertKeyUsageType[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||||
|
validity: { ttl: string };
|
||||||
|
notBefore?: Date;
|
||||||
|
notAfter?: Date;
|
||||||
|
};
|
||||||
|
template?: {
|
||||||
|
subject?: Array<{
|
||||||
|
type: string;
|
||||||
|
allowed?: string[];
|
||||||
|
required?: string[];
|
||||||
|
denied?: string[];
|
||||||
|
}>;
|
||||||
|
sans?: Array<{
|
||||||
|
type: string;
|
||||||
|
allowed?: string[];
|
||||||
|
required?: string[];
|
||||||
|
denied?: string[];
|
||||||
|
}>;
|
||||||
|
} | null;
|
||||||
|
profile?: { id: string; projectId: string } | null;
|
||||||
|
originalCert?: {
|
||||||
|
id: string;
|
||||||
|
friendlyName?: string | null;
|
||||||
|
commonName?: string | null;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
effectiveAlgorithms: {
|
||||||
|
signatureAlgorithm: CertSignatureAlgorithm;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
};
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "updateById">;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "encryptWithKmsKey" | "generateKmsKey">;
|
||||||
|
projectDAL: TProjectDALFactory;
|
||||||
|
tx: Parameters<TCertificateDALFactory["create"]>[1];
|
||||||
|
isRenewal?: boolean;
|
||||||
|
}) => {
|
||||||
|
const projectId = originalCert?.projectId || profile?.projectId;
|
||||||
|
if (!projectId) {
|
||||||
|
throw new BadRequestError({ message: "Project ID is required for certificate creation" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfSignedResult = await generateSelfSignedCertificate({
|
||||||
|
certificateRequest,
|
||||||
|
template,
|
||||||
|
effectiveSignatureAlgorithm: effectiveAlgorithms.signatureAlgorithm,
|
||||||
|
effectiveKeyAlgorithm: effectiveAlgorithms.keyAlgorithm
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateData = await createSelfSignedCertificateRecord({
|
||||||
|
selfSignedResult,
|
||||||
|
certificateRequest,
|
||||||
|
profile,
|
||||||
|
originalCert,
|
||||||
|
certificateDAL,
|
||||||
|
tx,
|
||||||
|
isRenewal
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateDAL.updateById(
|
||||||
|
certificateData.id,
|
||||||
|
{
|
||||||
|
signatureAlgorithm: effectiveAlgorithms.signatureAlgorithm,
|
||||||
|
keyAlgorithm: effectiveAlgorithms.keyAlgorithm
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await createEncryptedCertificateData({
|
||||||
|
certificateId: certificateData.id,
|
||||||
|
certificate: selfSignedResult.certificate,
|
||||||
|
privateKey: selfSignedResult.privateKey,
|
||||||
|
projectId,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
selfSignedResult,
|
||||||
|
certificateData
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const certificateV3ServiceFactory = ({
|
export const certificateV3ServiceFactory = ({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
@@ -359,7 +771,9 @@ export const certificateV3ServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
certificateSyncDAL,
|
certificateSyncDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue,
|
||||||
|
kmsService,
|
||||||
|
projectDAL
|
||||||
}: TCertificateV3ServiceFactoryDep) => {
|
}: TCertificateV3ServiceFactoryDep) => {
|
||||||
const issueCertificateFromProfile = async ({
|
const issueCertificateFromProfile = async ({
|
||||||
profileId,
|
profileId,
|
||||||
@@ -416,15 +830,6 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
|
||||||
if (!ca) {
|
|
||||||
throw new NotFoundError({ message: "Certificate Authority not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
validateCaSupport(ca, "direct certificate issuance");
|
|
||||||
|
|
||||||
validateAlgorithmCompatibility(ca, template);
|
|
||||||
|
|
||||||
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
|
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
|
||||||
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm as CertKeyAlgorithm | undefined;
|
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm as CertKeyAlgorithm | undefined;
|
||||||
|
|
||||||
@@ -440,12 +845,76 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.subject);
|
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template?.subject);
|
||||||
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
|
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
|
||||||
{ subjectAlternativeNames: certificateRequest.altNames },
|
{ subjectAlternativeNames: certificateRequest.altNames },
|
||||||
template.sans
|
template?.sans
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const issuerType = profile?.issuerType || (profile?.caId ? IssuerType.CA : IssuerType.SELF_SIGNED);
|
||||||
|
|
||||||
|
if (issuerType === IssuerType.SELF_SIGNED) {
|
||||||
|
const result = await certificateDAL.transaction(async (tx) => {
|
||||||
|
const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
|
||||||
|
|
||||||
|
return processSelfSignedCertificate({
|
||||||
|
certificateRequest,
|
||||||
|
template,
|
||||||
|
profile,
|
||||||
|
effectiveAlgorithms,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const { selfSignedResult, certificateData } = result;
|
||||||
|
|
||||||
|
const subjectCommonName =
|
||||||
|
(selfSignedResult.certificateSubject.common_name as string) ||
|
||||||
|
certificateRequest.commonName ||
|
||||||
|
"Self-signed Certificate";
|
||||||
|
|
||||||
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
||||||
|
profile,
|
||||||
|
certificateRequest.validity.ttl,
|
||||||
|
selfSignedResult.notAfter
|
||||||
|
);
|
||||||
|
|
||||||
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
|
await certificateDAL.updateById(certificateData.id, {
|
||||||
|
renewBeforeDays: finalRenewBeforeDays
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: selfSignedResult.certificate.toString("utf8"),
|
||||||
|
issuingCaCertificate: "",
|
||||||
|
certificateChain: selfSignedResult.certificate.toString("utf8"),
|
||||||
|
privateKey: selfSignedResult.privateKey.toString("utf8"),
|
||||||
|
serialNumber: selfSignedResult.serialNumber,
|
||||||
|
certificateId: certificateData.id,
|
||||||
|
projectId: profile.projectId,
|
||||||
|
profileName: profile.slug,
|
||||||
|
commonName: subjectCommonName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority ID not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCaSupport(ca, "direct certificate issuance");
|
||||||
|
validateAlgorithmCompatibility(ca, template);
|
||||||
|
|
||||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
||||||
await internalCaService.issueCertFromCa({
|
await internalCaService.issueCertFromCa({
|
||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
@@ -477,10 +946,11 @@ export const certificateV3ServiceFactory = ({
|
|||||||
new Date(cert.notAfter)
|
new Date(cert.notAfter)
|
||||||
);
|
);
|
||||||
|
|
||||||
await certificateDAL.updateById(cert.id, {
|
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
|
||||||
profileId,
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
renewBeforeDays: finalRenewBeforeDays
|
updateData.renewBeforeDays = finalRenewBeforeDays;
|
||||||
});
|
}
|
||||||
|
await certificateDAL.updateById(cert.id, updateData);
|
||||||
|
|
||||||
let finalCertificateChain = bufferToString(certificateChain);
|
let finalCertificateChain = bufferToString(certificateChain);
|
||||||
if (removeRootsFromChain) {
|
if (removeRootsFromChain) {
|
||||||
@@ -525,6 +995,12 @@ export const certificateV3ServiceFactory = ({
|
|||||||
enrollmentType
|
enrollmentType
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for CSR signing"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
throw new NotFoundError({ message: "Certificate Authority not found" });
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
@@ -592,10 +1068,11 @@ export const certificateV3ServiceFactory = ({
|
|||||||
|
|
||||||
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter));
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter));
|
||||||
|
|
||||||
await certificateDAL.updateById(cert.id, {
|
const updateData2: { profileId: string; renewBeforeDays?: number } = { profileId };
|
||||||
profileId,
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
renewBeforeDays: finalRenewBeforeDays
|
updateData2.renewBeforeDays = finalRenewBeforeDays;
|
||||||
});
|
}
|
||||||
|
await certificateDAL.updateById(cert.id, updateData2);
|
||||||
|
|
||||||
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
|
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
|
||||||
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
|
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
|
||||||
@@ -640,10 +1117,25 @@ export const certificateV3ServiceFactory = ({
|
|||||||
commonName: certificateOrder.commonName,
|
commonName: certificateOrder.commonName,
|
||||||
keyUsages: certificateOrder.keyUsages,
|
keyUsages: certificateOrder.keyUsages,
|
||||||
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
||||||
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
|
subjectAlternativeNames: certificateOrder.altNames.map((san) => {
|
||||||
type: san.type === "dns" ? CertSubjectAlternativeNameType.DNS_NAME : CertSubjectAlternativeNameType.IP_ADDRESS,
|
let certType: CertSubjectAlternativeNameType;
|
||||||
|
switch (san.type) {
|
||||||
|
case "dns":
|
||||||
|
certType = CertSubjectAlternativeNameType.DNS_NAME;
|
||||||
|
break;
|
||||||
|
case "ip":
|
||||||
|
certType = CertSubjectAlternativeNameType.IP_ADDRESS;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: certType,
|
||||||
value: san.value
|
value: san.value
|
||||||
})),
|
};
|
||||||
|
}),
|
||||||
validity: certificateOrder.validity,
|
validity: certificateOrder.validity,
|
||||||
notBefore: certificateOrder.notBefore,
|
notBefore: certificateOrder.notBefore,
|
||||||
notAfter: certificateOrder.notAfter,
|
notAfter: certificateOrder.notAfter,
|
||||||
@@ -663,6 +1155,12 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!profile.caId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Self-signed certificates are not supported for certificate ordering"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
throw new NotFoundError({ message: "Certificate Authority not found" });
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
@@ -741,16 +1239,20 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
|
let profile = null;
|
||||||
|
if (originalCert.profileId) {
|
||||||
|
profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile.enrollmentType !== EnrollmentType.API) {
|
if (profile.enrollmentType !== EnrollmentType.API) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint"
|
message:
|
||||||
|
"Certificate is not eligible for renewal: Only certificates issued from an API enrollment profile can be renewed through this endpoint"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const certificateSecret = await certificateSecretDAL.findOne({ certId: originalCert.id }, tx);
|
const certificateSecret = await certificateSecretDAL.findOne({ certId: originalCert.id }, tx);
|
||||||
if (!certificateSecret) {
|
if (!certificateSecret) {
|
||||||
@@ -761,10 +1263,11 @@ export const certificateV3ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!internal) {
|
if (!internal) {
|
||||||
|
const projectId = profile?.projectId || originalCert.projectId;
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: profile.projectId,
|
projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
@@ -776,7 +1279,16 @@ export const certificateV3ServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
|
const issuerType = profile?.issuerType || (originalCert.caId ? IssuerType.CA : IssuerType.SELF_SIGNED);
|
||||||
|
|
||||||
|
let ca;
|
||||||
|
if (issuerType === IssuerType.CA) {
|
||||||
|
const caId = profile?.caId || originalCert.caId;
|
||||||
|
if (!caId) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority ID not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
throw new NotFoundError({ message: "Certificate Authority not found" });
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
}
|
}
|
||||||
@@ -792,17 +1304,21 @@ export const certificateV3ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateCaSupport(ca, "direct certificate issuance");
|
validateCaSupport(ca, "direct certificate issuance");
|
||||||
|
}
|
||||||
|
|
||||||
const template = await certificateTemplateV2Service.getTemplateV2ById({
|
const templateId = profile?.certificateTemplateId || originalCert.certificateTemplateId;
|
||||||
|
const template = templateId
|
||||||
|
? await certificateTemplateV2Service.getTemplateV2ById({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
templateId: profile.certificateTemplateId,
|
templateId,
|
||||||
internal
|
internal
|
||||||
});
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
if (!template) {
|
if (!template && profile) {
|
||||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,10 +1373,13 @@ export const certificateV3ServiceFactory = ({
|
|||||||
keyAlgorithm: originalCert.keyAlgorithm || undefined
|
keyAlgorithm: originalCert.keyAlgorithm || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
let validationResult: { isValid: boolean; errors: string[] } = { isValid: true, errors: [] };
|
||||||
|
if (profile?.certificateTemplateId) {
|
||||||
|
validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||||
profile.certificateTemplateId,
|
profile.certificateTemplateId,
|
||||||
certificateRequest
|
certificateRequest
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!validationResult.isValid) {
|
if (!validationResult.isValid) {
|
||||||
await certificateDAL.updateById(originalCert.id, {
|
await certificateDAL.updateById(originalCert.id, {
|
||||||
@@ -872,14 +1391,28 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
validateAlgorithmCompatibility(ca, template);
|
|
||||||
const notBefore = new Date();
|
const notBefore = new Date();
|
||||||
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
|
const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, ttl, notAfter);
|
const finalRenewBeforeDays = profile ? calculateFinalRenewBeforeDays(profile, ttl, notAfter) : undefined;
|
||||||
|
|
||||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
let certificate: string;
|
||||||
await internalCaService.issueCertFromCa({
|
let certificateChain: string;
|
||||||
|
let issuingCaCertificate: string;
|
||||||
|
let serialNumber: string;
|
||||||
|
let newCert: TCertificates;
|
||||||
|
|
||||||
|
if (issuerType === IssuerType.CA) {
|
||||||
|
// CA-signed certificate renewal
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority not found for CA-signed certificate renewal" });
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAlgorithmCompatibility(ca, {
|
||||||
|
algorithms: template?.algorithms
|
||||||
|
} as { algorithms?: { signature?: string[] } });
|
||||||
|
|
||||||
|
const caResult = await internalCaService.issueCertFromCa({
|
||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate",
|
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate",
|
||||||
commonName: originalCert.commonName || "",
|
commonName: originalCert.commonName || "",
|
||||||
@@ -900,20 +1433,72 @@ export const certificateV3ServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
|
|
||||||
const newCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
|
certificate = caResult.certificate;
|
||||||
|
certificateChain = caResult.certificateChain;
|
||||||
|
issuingCaCertificate = caResult.issuingCaCertificate;
|
||||||
|
serialNumber = caResult.serialNumber;
|
||||||
|
|
||||||
|
const foundCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
|
||||||
|
if (!foundCert) {
|
||||||
|
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||||
|
}
|
||||||
|
newCert = foundCert;
|
||||||
|
} else {
|
||||||
|
// Self-signed certificate renewal
|
||||||
|
const effectiveAlgorithms = getEffectiveAlgorithms(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
originalSignatureAlgorithm,
|
||||||
|
originalKeyAlgorithm
|
||||||
|
);
|
||||||
|
|
||||||
|
const selfSignedRenewalResult = await processSelfSignedCertificate({
|
||||||
|
certificateRequest,
|
||||||
|
template,
|
||||||
|
profile,
|
||||||
|
originalCert,
|
||||||
|
effectiveAlgorithms,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
tx,
|
||||||
|
isRenewal: true
|
||||||
|
});
|
||||||
|
|
||||||
|
certificate = selfSignedRenewalResult.selfSignedResult.certificate.toString("utf8");
|
||||||
|
certificateChain = selfSignedRenewalResult.selfSignedResult.certificate.toString("utf8"); // Self-signed has no chain
|
||||||
|
issuingCaCertificate = ""; // No issuing CA for self-signed
|
||||||
|
serialNumber = selfSignedRenewalResult.selfSignedResult.serialNumber;
|
||||||
|
newCert = selfSignedRenewalResult.certificateData;
|
||||||
|
}
|
||||||
|
|
||||||
if (!newCert) {
|
if (!newCert) {
|
||||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await certificateDAL.updateById(
|
// For self-signed certificates, we already set the renewal data during creation
|
||||||
newCert.id,
|
// For CA-signed certificates, we need to set it now
|
||||||
{
|
if (issuerType === IssuerType.CA) {
|
||||||
profileId: originalCert.profileId,
|
const renewalUpdateData: {
|
||||||
renewBeforeDays: finalRenewBeforeDays,
|
profileId: string | null;
|
||||||
|
renewedFromCertificateId: string;
|
||||||
|
renewBeforeDays?: number;
|
||||||
|
} = {
|
||||||
|
profileId: originalCert.profileId || null,
|
||||||
renewedFromCertificateId: originalCert.id
|
renewedFromCertificateId: originalCert.id
|
||||||
},
|
};
|
||||||
tx
|
|
||||||
);
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
|
renewalUpdateData.renewBeforeDays = finalRenewBeforeDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
await certificateDAL.updateById(newCert.id, renewalUpdateData, tx);
|
||||||
|
} else if (finalRenewBeforeDays !== undefined) {
|
||||||
|
// For self-signed certificates, just update the renewBeforeDays if needed
|
||||||
|
await certificateDAL.updateById(newCert.id, { renewBeforeDays: finalRenewBeforeDays }, tx);
|
||||||
|
}
|
||||||
|
|
||||||
await certificateDAL.updateById(
|
await certificateDAL.updateById(
|
||||||
originalCert.id,
|
originalCert.id,
|
||||||
@@ -953,8 +1538,8 @@ export const certificateV3ServiceFactory = ({
|
|||||||
certificateChain: finalCertificateChain,
|
certificateChain: finalCertificateChain,
|
||||||
serialNumber: renewalResult.serialNumber,
|
serialNumber: renewalResult.serialNumber,
|
||||||
certificateId: renewalResult.newCert.id,
|
certificateId: renewalResult.newCert.id,
|
||||||
projectId: renewalResult.profile.projectId,
|
projectId: renewalResult.originalCert.projectId,
|
||||||
profileName: renewalResult.profile.slug,
|
profileName: renewalResult.profile?.slug || "Self-signed Certificate",
|
||||||
commonName: renewalResult.originalCert.commonName || ""
|
commonName: renewalResult.originalCert.commonName || ""
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -309,6 +309,14 @@ export const certificateServiceFactory = ({
|
|||||||
|
|
||||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
|
if (!certBody) {
|
||||||
|
throw new NotFoundError({ message: "Certificate body not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certBody.encryptedCertificate) {
|
||||||
|
throw new BadRequestError({ message: "Certificate data not available" });
|
||||||
|
}
|
||||||
|
|
||||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||||
projectId: cert.projectId,
|
projectId: cert.projectId,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -599,6 +607,14 @@ export const certificateServiceFactory = ({
|
|||||||
|
|
||||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||||
|
|
||||||
|
if (!certBody) {
|
||||||
|
throw new NotFoundError({ message: "Certificate body not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certBody.encryptedCertificate) {
|
||||||
|
throw new BadRequestError({ message: "Certificate data not available" });
|
||||||
|
}
|
||||||
|
|
||||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||||
projectId: cert.projectId,
|
projectId: cert.projectId,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
|||||||
@@ -270,7 +270,13 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
const tokenReviewerJwtSnippet = `${tokenReviewerJwt?.substring?.(0, 10) || ""}...${tokenReviewerJwt?.substring?.(tokenReviewerJwt.length - 10) || ""}`;
|
||||||
|
const serviceAccountJwtSnippet = `${serviceAccountJwt?.substring?.(0, 10) || ""}...${serviceAccountJwt?.substring?.(serviceAccountJwt.length - 10) || ""}`;
|
||||||
if (err instanceof AxiosError) {
|
if (err instanceof AxiosError) {
|
||||||
|
logger.error(
|
||||||
|
{ response: err.response, host, port, tokenReviewerJwtSnippet, serviceAccountJwtSnippet },
|
||||||
|
"tokenReviewCallbackRaw: Kubernetes token review request error (request error)"
|
||||||
|
);
|
||||||
if (err.response) {
|
if (err.response) {
|
||||||
const { message } = err?.response?.data as unknown as { message?: string };
|
const { message } = err?.response?.data as unknown as { message?: string };
|
||||||
|
|
||||||
@@ -281,6 +287,11 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
{ error: err as Error, host, port, tokenReviewerJwtSnippet, serviceAccountJwtSnippet },
|
||||||
|
"tokenReviewCallbackRaw: Kubernetes token review request error (non-request error)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ export enum IntegrationUrls {
|
|||||||
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
|
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
|
||||||
|
|
||||||
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations",
|
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations",
|
||||||
CHEF_API_URL = "https://api.chef.io"
|
CHEF_API_URL = "https://api.chef.io",
|
||||||
|
DNS_MADE_EASY_API_URL = "https://api.dnsmadeeasy.com",
|
||||||
|
DNS_MADE_EASY_SANDBOX_API_URL = "https://api.sandbox.dnsmadeeasy.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getIntegrationOptions = async () => {
|
export const getIntegrationOptions = async () => {
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"),
|
db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"),
|
||||||
|
|
||||||
db.ref("slug").withSchema(TableName.Role).as("roleSlug"),
|
db.ref("slug").withSchema(TableName.Role).as("roleSlug"),
|
||||||
|
db.ref("name").withSchema(TableName.Role).as("roleName"),
|
||||||
db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"),
|
db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"),
|
||||||
db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"),
|
db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"),
|
||||||
db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"),
|
db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"),
|
||||||
@@ -180,6 +181,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
|
|||||||
label: "roles" as const,
|
label: "roles" as const,
|
||||||
mapper: ({
|
mapper: ({
|
||||||
roleSlug,
|
roleSlug,
|
||||||
|
roleName,
|
||||||
membershipRoleId,
|
membershipRoleId,
|
||||||
membershipRole,
|
membershipRole,
|
||||||
membershipRoleIsTemporary,
|
membershipRoleIsTemporary,
|
||||||
@@ -193,6 +195,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
|
|||||||
id: membershipRoleId,
|
id: membershipRoleId,
|
||||||
role: membershipRole,
|
role: membershipRole,
|
||||||
customRoleSlug: roleSlug,
|
customRoleSlug: roleSlug,
|
||||||
|
customRoleName: roleName,
|
||||||
temporaryRange: membershipRoleTemporaryRange,
|
temporaryRange: membershipRoleTemporaryRange,
|
||||||
temporaryMode: membershipRoleTemporaryMode,
|
temporaryMode: membershipRoleTemporaryMode,
|
||||||
temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime,
|
temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime,
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Heading, Section, Text } from "@react-email/components";
|
||||||
|
|
||||||
|
import { BaseButton } from "./BaseButton";
|
||||||
|
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||||
|
|
||||||
|
interface DynamicSecretLeaseRevocationFailedTemplateProps
|
||||||
|
extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
|
||||||
|
siteUrl: string;
|
||||||
|
dynamicSecretLeaseUrl: string;
|
||||||
|
dynamicSecretName: string;
|
||||||
|
projectName: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
errorMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DynamicSecretLeaseRevocationFailedTemplate = ({
|
||||||
|
siteUrl,
|
||||||
|
dynamicSecretLeaseUrl,
|
||||||
|
dynamicSecretName,
|
||||||
|
projectName,
|
||||||
|
environmentSlug,
|
||||||
|
errorMessage
|
||||||
|
}: DynamicSecretLeaseRevocationFailedTemplateProps) => {
|
||||||
|
return (
|
||||||
|
<BaseEmailWrapper
|
||||||
|
title="Dynamic Secret Lease Revocation Failed"
|
||||||
|
preview={`Dynamic secret lease revocation failed for dynamic secret ${dynamicSecretName}`}
|
||||||
|
siteUrl={siteUrl}
|
||||||
|
>
|
||||||
|
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||||
|
Dynamic Secret Lease Revocation Failed
|
||||||
|
</Heading>
|
||||||
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
One or more leases for the dynamic secret <strong>{dynamicSecretName}</strong> in project{" "}
|
||||||
|
<strong>{projectName}</strong> and environment <strong>{environmentSlug}</strong> have failed to revoke after
|
||||||
|
multiple attempts.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Please review the dynamic secret leases and attempt to revoke them again.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="mt-[24px] bg-gray-50 pt-[2px] mb-[25px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||||
|
<Text className="mb-[0px]">
|
||||||
|
<strong>Latest error message</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="leading-[24px] text-[14px] text-red-600 mt-[4px]">{errorMessage}</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="text-center">
|
||||||
|
<BaseButton href={dynamicSecretLeaseUrl}>View Dynamic Secret Leases</BaseButton>
|
||||||
|
</Section>
|
||||||
|
</BaseEmailWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicSecretLeaseRevocationFailedTemplate;
|
||||||
|
|
||||||
|
DynamicSecretLeaseRevocationFailedTemplate.PreviewProps = {
|
||||||
|
errorMessage: 'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "[REDACTED]" - tuple concurrently updated.',
|
||||||
|
dynamicSecretLeaseUrl: "https://infisical.com/test",
|
||||||
|
leaseId: "717d5013-7194-49d9-b6ac-6192328c2914",
|
||||||
|
dynamicSecretName: "postgres-prod-db",
|
||||||
|
projectName: "Development Team",
|
||||||
|
environmentSlug: "dev",
|
||||||
|
siteUrl: "https://infisical.com"
|
||||||
|
} as DynamicSecretLeaseRevocationFailedTemplateProps;
|
||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
SubOrganizationInvitationTemplate,
|
SubOrganizationInvitationTemplate,
|
||||||
UnlockAccountTemplate
|
UnlockAccountTemplate
|
||||||
} from "./emails";
|
} from "./emails";
|
||||||
|
import DynamicSecretLeaseRevocationFailedTemplate from "./emails/DynamicSecretLeaseRevocationFailedTemplate";
|
||||||
|
|
||||||
export type TSmtpConfig = SMTPTransport.Options;
|
export type TSmtpConfig = SMTPTransport.Options;
|
||||||
export type TSmtpSendMail = {
|
export type TSmtpSendMail = {
|
||||||
@@ -89,7 +90,8 @@ export enum SmtpTemplates {
|
|||||||
SecretScanningV2ScanFailed = "secretScanningV2ScanFailed",
|
SecretScanningV2ScanFailed = "secretScanningV2ScanFailed",
|
||||||
SecretScanningV2SecretsDetected = "secretScanningV2SecretsDetected",
|
SecretScanningV2SecretsDetected = "secretScanningV2SecretsDetected",
|
||||||
AccountDeletionConfirmation = "accountDeletionConfirmation",
|
AccountDeletionConfirmation = "accountDeletionConfirmation",
|
||||||
HealthAlert = "healthAlert"
|
HealthAlert = "healthAlert",
|
||||||
|
DynamicSecretLeaseRevocationFailed = "dynamicSecretLeaseRevocationFailed"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SmtpHost {
|
export enum SmtpHost {
|
||||||
@@ -137,7 +139,8 @@ const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
|
|||||||
[SmtpTemplates.SecretScanningV2ScanFailed]: SecretScanningScanFailedTemplate,
|
[SmtpTemplates.SecretScanningV2ScanFailed]: SecretScanningScanFailedTemplate,
|
||||||
[SmtpTemplates.SecretScanningV2SecretsDetected]: SecretScanningSecretsDetectedTemplate,
|
[SmtpTemplates.SecretScanningV2SecretsDetected]: SecretScanningSecretsDetectedTemplate,
|
||||||
[SmtpTemplates.AccountDeletionConfirmation]: AccountDeletionConfirmationTemplate,
|
[SmtpTemplates.AccountDeletionConfirmation]: AccountDeletionConfirmationTemplate,
|
||||||
[SmtpTemplates.HealthAlert]: HealthAlertTemplate
|
[SmtpTemplates.HealthAlert]: HealthAlertTemplate,
|
||||||
|
[SmtpTemplates.DynamicSecretLeaseRevocationFailed]: DynamicSecretLeaseRevocationFailedTemplate
|
||||||
};
|
};
|
||||||
|
|
||||||
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
||||||
|
|||||||
@@ -118,6 +118,7 @@
|
|||||||
"integrations/app-connections/cloudflare",
|
"integrations/app-connections/cloudflare",
|
||||||
"integrations/app-connections/databricks",
|
"integrations/app-connections/databricks",
|
||||||
"integrations/app-connections/digital-ocean",
|
"integrations/app-connections/digital-ocean",
|
||||||
|
"integrations/app-connections/dns-made-easy",
|
||||||
"integrations/app-connections/flyio",
|
"integrations/app-connections/flyio",
|
||||||
"integrations/app-connections/gcp",
|
"integrations/app-connections/gcp",
|
||||||
"integrations/app-connections/github",
|
"integrations/app-connections/github",
|
||||||
@@ -490,6 +491,10 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"integrations/platforms/ansible",
|
"integrations/platforms/ansible",
|
||||||
"integrations/platforms/apache-airflow",
|
"integrations/platforms/apache-airflow",
|
||||||
|
{
|
||||||
|
"group": "AWS",
|
||||||
|
"pages": ["integrations/platforms/aws/lambda"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Kubernetes Operator",
|
"group": "Kubernetes Operator",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -752,7 +757,7 @@
|
|||||||
{
|
{
|
||||||
"group": "Infrastructure Integrations",
|
"group": "Infrastructure Integrations",
|
||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/pki/pki-issuer",
|
"documentation/platform/pki/k8s-cert-manager",
|
||||||
"documentation/platform/pki/integration-guides/gloo-mesh",
|
"documentation/platform/pki/integration-guides/gloo-mesh",
|
||||||
"documentation/platform/pki/integration-guides/windows-server-acme",
|
"documentation/platform/pki/integration-guides/windows-server-acme",
|
||||||
"documentation/platform/pki/integration-guides/nginx-certbot",
|
"documentation/platform/pki/integration-guides/nginx-certbot",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Infisical offers a non-exhaustive set of clients and interfaces to support a wid
|
|||||||
|
|
||||||
- [External Secrets Operator (ESO)](https://external-secrets.io/latest/provider/infisical): Allows Infisical to act as a backend provider for syncing secrets into Kubernetes `Secret` objects using the widely adopted External Secrets Operator.
|
- [External Secrets Operator (ESO)](https://external-secrets.io/latest/provider/infisical): Allows Infisical to act as a backend provider for syncing secrets into Kubernetes `Secret` objects using the widely adopted External Secrets Operator.
|
||||||
|
|
||||||
- [Kubernetes PKI Issuer](/documentation/platform/pki/pki-issuer): A controller that issues X.509 certificates from Infisical PKI using the cert-manager Issuer and Certificate CRDs.
|
- [Kubernetes cert-manager](/documentation/platform/pki/k8s-cert-manager): A controller that issues X.509 certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles) using the cert-manager Issuer and Certificate CRDs.
|
||||||
|
|
||||||
- [Secret Syncs](/integrations/secret-syncs/overview): Native integrations to forward secrets to services like GitHub, GitLab, AWS Secrets Manager, Vercel, and more.
|
- [Secret Syncs](/integrations/secret-syncs/overview): Native integrations to forward secrets to services like GitHub, GitLab, AWS Secrets Manager, Vercel, and more.
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ their **ACME Directory URL** such as:
|
|||||||
- ZeroSSL: `https://acme.zerossl.com/v2/DV90`.
|
- ZeroSSL: `https://acme.zerossl.com/v2/DV90`.
|
||||||
- SSL.com: `https://acme.ssl.com/sslcom-dv-rsa`.
|
- SSL.com: `https://acme.ssl.com/sslcom-dv-rsa`.
|
||||||
|
|
||||||
When Infisical requests a certificate from an ACME-compatible CA, it creates a TXT record at `_acme-challenge.{your-domain}` in your configured DNS provider (e.g. Route53, Cloudflare, etc.); this TXT record contains the challenge token issued by the ACME-compatible CA to validate domain control for the requested certificate.
|
When Infisical requests a certificate from an ACME-compatible CA, it creates a TXT record at `_acme-challenge.{your-domain}` in your configured DNS provider (e.g. Route53, Cloudflare, DNS Made Easy, etc.); this TXT record contains the challenge token issued by the ACME-compatible CA to validate domain control for the requested certificate.
|
||||||
The ACME provider checks for the existence of this TXT record to verify domain control before issuing the certificate back to Infisical.
|
The ACME provider checks for the existence of this TXT record to verify domain control before issuing the certificate back to Infisical.
|
||||||
|
|
||||||
After validation completes successfully, Infisical automatically removes the TXT record from your DNS provider.
|
After validation completes successfully, Infisical automatically removes the TXT record from your DNS provider.
|
||||||
@@ -120,6 +120,11 @@ In the following steps, we explore how to connect Infisical to an ACME-compatibl
|
|||||||
|
|
||||||
For detailed instructions on setting up a Cloudflare connection, see the [Cloudflare Connection](/integrations/app-connections/cloudflare) documentation.
|
For detailed instructions on setting up a Cloudflare connection, see the [Cloudflare Connection](/integrations/app-connections/cloudflare) documentation.
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab title="DNS Made Easy">
|
||||||
|
Navigate to your Certificate Management Project > App Connections and create a new DNS Made Easy connection.
|
||||||
|
|
||||||
|
For detailed instructions on setting up a DNS Made Easy connection, see the [DNS Made Easy Connection](/integrations/app-connections/dns-made-easy) documentation.
|
||||||
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Register an ACME-compatible CA">
|
<Step title="Register an ACME-compatible CA">
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ where you can manage various aspects of its lifecycle including deployment to cl
|
|||||||
|
|
||||||
## Guide to Issuing Certificates
|
## Guide to Issuing Certificates
|
||||||
|
|
||||||
To issue a certificate, you must first create a [certificate profile](/documentation/platform/pki/certificates/profiles) and a [certificate template](/documentation/platform/pki/certificates/templates) to go along with it.
|
To [issue a certificate](/documentation/platform/pki/concepts/certificate-lifecycle#enrollment-request-%2F-issuance), you must first create a [certificate profile](/documentation/platform/pki/certificates/profiles) and a [certificate template](/documentation/platform/pki/certificates/templates) to go along with it.
|
||||||
|
|
||||||
The [enrollment method](/documentation/platform/pki/enrollment-methods/overview) configured on the certificate profile determines how a certificate is issued for it.
|
- Self-Signed Certificates: To issue a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate), you must configure the certificate profile to use the `Self-Signed` issuer type. You can then use the [API enrollment method](/documentation/platform/pki/enrollment-methods/api) to request a self-signed certificate against it.
|
||||||
Refer to the documentation for each enrollment method to learn more about how to issue certificates using it.
|
- CA-Issued Certificates: To issue a certificate from a certificate authority, you must configure the certificate profile to use the `Certificate Authority` issuer type and select the [issuing CA](/documentation/platform/pki/ca/overview) to use. You can then use one of the [enrollment methods](/documentation/platform/pki/enrollment-methods/overview) to request a certificate against it.
|
||||||
|
|
||||||
|
Refer to the documentation for each [enrollment method](/documentation/platform/pki/enrollment-methods/overview) to learn more about how to issue certificates using it.
|
||||||
|
|
||||||
## Guide to Renewing Certificates
|
## Guide to Renewing Certificates
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ Here's some guidance on each field:
|
|||||||
|
|
||||||
- Name: A slug-friendly name for the profile such as `web-servers`.
|
- Name: A slug-friendly name for the profile such as `web-servers`.
|
||||||
- Description: An optional description for the profile.
|
- Description: An optional description for the profile.
|
||||||
- Issuing CA: The [issuing CA](/documentation/platform/pki/ca/overview) that should be used to issue certificates for the profile.
|
- Issuer Type: The type of issuer that should be used to issue certificates for the profile; this can be either `Certificate Authority` or `Self-Signed`. If `Self-Signed` is selected, then the profile will only support the API enrollment method and be used to issue self-signed certificates over REST API.
|
||||||
|
- Issuing CA: The [issuing CA](/documentation/platform/pki/ca/overview) that should be used to issue certificates for the profile when the **Issuer Type** is set to `Certificate Authority`.
|
||||||
- Certificate Template: The [certificate template](/documentation/platform/pki/certificates/templates) that should be used to validate certificate requests for the profile.
|
- Certificate Template: The [certificate template](/documentation/platform/pki/certificates/templates) that should be used to validate certificate requests for the profile.
|
||||||
- Enrollment Method: The enrollment method that should be used to enroll certificates for the profile such as ACME, EST, API, etc.
|
- Enrollment Method: The enrollment method that should be used to enroll certificates for the profile such as ACME, EST, API, etc.
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ sidebarTitle: "ACME"
|
|||||||
|
|
||||||
## Concept
|
## Concept
|
||||||
|
|
||||||
The ACME enrollment method allows you to issue and manage certificates against a specific [certificate profile](/documentation/platform/pki/certificates/profiles) using the [ACME protocol](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment).
|
The ACME enrollment method allows Infisical to act as an ACME server. It lets you request and manage certificates against a specific [certificate profile](/documentation/platform/pki/certificates/profiles) using the [ACME protocol](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment).
|
||||||
This method is suitable for web servers, load balancers, and other general-purpose servers that can run an [ACME client](https://letsencrypt.org/docs/client-options/) for automated certificate management.
|
This method is suitable for web servers, load balancers, and other general-purpose servers that can run an [ACME client](https://letsencrypt.org/docs/client-options/) for automated certificate management.
|
||||||
|
|
||||||
Infisical's ACME enrollment method is based on [RFC 8555](https://datatracker.ietf.org/doc/html/rfc8555/).
|
Infisical's ACME enrollment method is based on [RFC 8555](https://datatracker.ietf.org/doc/html/rfc8555/).
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: "Apache Server"
|
title: "Apache Server"
|
||||||
description: "Learn how to issue SSL/TLS certificates from Infisical using ACME enrollment on Apache Server with Certbot"
|
description: "Learn how to issue TLS certificates from Infisical using ACME enrollment on Apache Server with Certbot"
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide demonstrates how to use Infisical to issue SSL/TLS certificates for your [Apache HTTP Server](https://httpd.apache.org/).
|
This guide demonstrates how to use Infisical to issue TLS certificates for your [Apache HTTP Server](https://httpd.apache.org/).
|
||||||
|
|
||||||
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). Apache benefits from excellent Certbot integration, allowing both certificate-only mode and automatic SSL configuration.
|
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). Apache benefits from excellent Certbot integration, allowing both certificate-only mode and automatic SSL configuration.
|
||||||
|
|
||||||
@@ -182,4 +182,5 @@ Before you begin, make sure you have:
|
|||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
title: "Gloo Mesh"
|
title: "Gloo Mesh"
|
||||||
description: "Learn how to automatically provision and manage Istio intermediate CA certificates for Gloo Mesh using Infisical PKI"
|
description: "Learn how to automatically provision and manage Istio intermediate CA certificates for Gloo Mesh using Infisical"
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide will provide a high level overview on how you can use Infisical PKI and cert-manager to issue Istio intermediate CA certificates for your Gloo Mesh workload clusters. For more background about Istio certificates, see the [Istio CA overview](https://istio.io/latest/docs/concepts/security/#pki).
|
This guide will provide a high level overview on how you can use Infisical and [cert-manager](https://cert-manager.io/) to issue Istio intermediate CA certificates for your Gloo Mesh workload clusters. For more background about Istio certificates, see the [Istio CA overview](https://istio.io/latest/docs/concepts/security/#pki).
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
In this setup, we will use Infisical PKI to generate and store your root CA and subordinate CAs that are used to generate Istio intermediate CAs for your Gloo Mesh workload clusters.
|
In this setup, we will use Infisical to generate and store your root CA and subordinate CAs that are used to generate Istio intermediate CAs for your Gloo Mesh workload clusters.
|
||||||
To manage the lifecycle of Istio intermediate CA certificates, you'll also install [cert-manager](https://cert-manager.io/).
|
To manage the lifecycle of Istio intermediate CA certificates, you'll also install [cert-manager](https://cert-manager.io/).
|
||||||
Cert-manager is a Kubernetes controller that helps you automate the process of obtaining and renewing certificates from various PKI providers.
|
Cert-manager is a Kubernetes controller that helps you automate the process of obtaining and renewing certificates from various PKI providers.
|
||||||
|
|
||||||
@@ -21,19 +21,19 @@ With this approach, you get the following benefits:
|
|||||||
## General Setup
|
## General Setup
|
||||||
|
|
||||||
The certificate provisioning workflow begins with setting up your PKI hierarchy in Infisical, where you create root and subordinate certificate authorities.
|
The certificate provisioning workflow begins with setting up your PKI hierarchy in Infisical, where you create root and subordinate certificate authorities.
|
||||||
When you deploy a `Certificate` CRD in your workload cluster, `cert-manager` uses the Infisical PKI Issuer controller to authenticate with Infisical using machine identity credentials and request an intermediate CA certificate.
|
When you deploy a `Certificate` CRD in your workload cluster, `cert-manager` uses the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles) to authenticate using EAB credentials and request an intermediate CA certificate.
|
||||||
Infisical verifies the request against your certificate templates and returns the signed certificate.
|
Infisical verifies the request against your certificate templates and returns the signed certificate.
|
||||||
From there, Istio's control plane will automatically use this intermediate CA to sign leaf certificates for workloads in the service mesh, enabling secure mTLS communication across your entire Gloo Mesh infrastructure.
|
From there, Istio's control plane will automatically use this intermediate CA to sign leaf certificates for workloads in the service mesh, enabling secure mTLS communication across your entire Gloo Mesh infrastructure.
|
||||||
|
|
||||||
Follow the [Infisical PKI Issuer guide](/documentation/platform/pki/pki-issuer) for detailed instructions on how to set up the Infisical PKI Issuer and cert-manager for your Istio intermediate CA certificates in Gloo Mesh clusters.
|
Follow the [Kubernetes cert-manager guide](/documentation/platform/pki/k8s-cert-manager) for detailed instructions on how to set up the Infisical and cert-manager for your Istio intermediate CA certificates in Gloo Mesh clusters.
|
||||||
|
|
||||||
For Gloo Mesh-specific configuration, ensure that:
|
For Gloo Mesh-specific configuration, ensure that:
|
||||||
|
|
||||||
- The Certificate resource targets the `istio-system` namespace with `secretName: cacerts`
|
- The Certificate resource targets the `istio-system` namespace with `secretName: cacerts`
|
||||||
- Certificate templates in Infisical PKI are configured for intermediate CA usage with appropriate key usage and constraints
|
- Certificate profiles in Infisical are configured for intermediate CA usage with appropriate key usage and constraints
|
||||||
- Multiple workload clusters use the same Infisical PKI root to enable cross-cluster mTLS communication
|
- Multiple workload clusters use the same Infisical root to enable cross-cluster mTLS communication
|
||||||
|
|
||||||
## Using the certificates
|
## Using the certificates
|
||||||
|
|
||||||
Once the `cacerts` Kubernetes secret is created in the `istio-system` namespace, Istio automatically uses the custom CA certificate instead of the default self-signed certificate.
|
Once the `cacerts` Kubernetes secret is created in the `istio-system` namespace, Istio automatically uses the custom CA certificate instead of the default self-signed certificate.
|
||||||
When you deploy applications to your Gloo Mesh service mesh, the workloads will receive leaf certificates signed by your Infisical PKI intermediate CA, enabling secure mTLS communication across your entire mesh infrastructure.
|
When you deploy applications to your Gloo Mesh service mesh, the workloads will receive leaf certificates signed by your Infisical intermediate CA, enabling secure mTLS communication across your entire mesh infrastructure.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: "JBoss/WildFly"
|
title: "JBoss/WildFly"
|
||||||
description: "Learn how to issue SSL/TLS certificates from Infisical using ACME enrollment on JBoss/WildFly with Certbot"
|
description: "Learn how to issue TLS certificates from Infisical using ACME enrollment on JBoss/WildFly with Certbot"
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide demonstrates how to use Infisical to issue SSL/TLS certificates for your [JBoss](https://www.jboss.org/)/[WildFly](https://wildfly.org/) application server.
|
This guide demonstrates how to use Infisical to issue TLS certificates for your [JBoss](https://www.jboss.org/)/[WildFly](https://wildfly.org/) application server.
|
||||||
|
|
||||||
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). JBoss/WildFly requires certificates in Java keystore format, which this guide addresses through the certificate conversion process.
|
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). JBoss/WildFly requires certificates in Java keystore format, which this guide addresses through the certificate conversion process.
|
||||||
|
|
||||||
@@ -223,4 +223,5 @@ Before you begin, make sure you have:
|
|||||||
Certbot automatically renews certificates when they are within 30 days of expiration using its built-in systemd timer. The deploy hook above will run after each successful renewal, handling the keystore conversion and service restart automatically. Because JBoss/WildFly requires the standalone authenticator (which stops the service temporarily), plan for brief service interruptions during renewal.
|
Certbot automatically renews certificates when they are within 30 days of expiration using its built-in systemd timer. The deploy hook above will run after each successful renewal, handling the keystore conversion and service restart automatically. Because JBoss/WildFly requires the standalone authenticator (which stops the service temporarily), plan for brief service interruptions during renewal.
|
||||||
</Note>
|
</Note>
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: "Nginx"
|
title: "Nginx"
|
||||||
description: "Learn how to issue SSL/TLS certificates from Infisical using ACME enrollment on Nginx with Certbot"
|
description: "Learn how to issue TLS certificates from Infisical using ACME enrollment on Nginx with Certbot"
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide demonstrates how to use Infisical to issue SSL/TLS certificates for your [Nginx](https://nginx.org/) server.
|
This guide demonstrates how to use Infisical to issue TLS certificates for your [Nginx](https://nginx.org/) server.
|
||||||
|
|
||||||
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles).
|
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles).
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: "Tomcat"
|
title: "Tomcat"
|
||||||
description: "Learn how to issue SSL/TLS certificates from Infisical using ACME enrollment on Tomcat with Certbot"
|
description: "Learn how to issue TLS certificates from Infisical using ACME enrollment on Tomcat with Certbot"
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide demonstrates how to use Infisical to issue SSL/TLS certificates for your [Apache Tomcat](https://tomcat.apache.org/) application server.
|
This guide demonstrates how to use Infisical to issue TLS certificates for your [Apache Tomcat](https://tomcat.apache.org/) application server.
|
||||||
|
|
||||||
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). Unlike web servers with native Certbot plugins, Tomcat requires certificates to be manually configured after issuance.
|
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). Unlike web servers with native Certbot plugins, Tomcat requires certificates to be manually configured after issuance.
|
||||||
|
|
||||||
@@ -248,4 +248,5 @@ Before you begin, make sure you have:
|
|||||||
Since Tomcat reads certificates from the file system on startup, you only need to restart the service after certificate renewal. The certificate file paths in `/etc/letsencrypt/live/` are symbolic links that automatically point to the latest certificates.
|
Since Tomcat reads certificates from the file system on startup, you only need to restart the service after certificate renewal. The certificate file paths in `/etc/letsencrypt/live/` are symbolic links that automatically point to the latest certificates.
|
||||||
</Note>
|
</Note>
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: "Windows Server"
|
title: "Windows Server"
|
||||||
description: "Learn how to issue SSL/TLS certificates from Infisical using ACME enrollment on Windows Server with win-acme"
|
description: "Learn how to issue TLS certificates from Infisical using ACME enrollment on Windows Server with win-acme"
|
||||||
---
|
---
|
||||||
|
|
||||||
This guide demonstrates how to use Infisical to issue SSL/TLS certificates for your [Windows Server](https://www.microsoft.com/en-us/windows-server) environments.
|
This guide demonstrates how to use Infisical to issue TLS certificates for your [Windows Server](https://www.microsoft.com/en-us/windows-server) environments.
|
||||||
|
|
||||||
It uses [win-acme](https://www.win-acme.com/), a feature-rich [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client designed specifically for Windows, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). Win-acme offers excellent integration with IIS, Windows Certificate Store, and various certificate storage options.
|
It uses [win-acme](https://www.win-acme.com/), a feature-rich [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client designed specifically for Windows, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). Win-acme offers excellent integration with IIS, Windows Certificate Store, and various certificate storage options.
|
||||||
|
|
||||||
@@ -191,4 +191,5 @@ Before you begin, make sure you have:
|
|||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
---
|
||||||
|
title: "Kubernetes cert-manager"
|
||||||
|
description: "Learn how to automatically provision and manage TLS certificates in Kubernetes using Infisical"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
|
||||||
|
This guide demonstrates how to use Infisical to issue TLS certificates back to your Kubernetes environment using [cert-manager](https://cert-manager.io/).
|
||||||
|
|
||||||
|
It uses the [ACME issuer type](https://cert-manager.io/docs/configuration/acme/) to request and renew certificates automatically from Infisical
|
||||||
|
using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). The issuer is perfect at obtaining X.509 certificates for Ingresses and other Kubernetes resources and can automatically renew them before expiration.
|
||||||
|
|
||||||
|
The typical workflow involves installing `cert-manager` and configuring resources that represent the connection details to Infisical as well as the certificates you want to issue.
|
||||||
|
Each issued certificate and its corresponding private key are stored in a Kubernetes `Secret`.
|
||||||
|
|
||||||
|
We recommend reading the official [cert-manager documentation](https://cert-manager.io/docs/) for a complete overview.
|
||||||
|
For the ACME-specific configuration, refer to the [ACME section](https://cert-manager.io/docs/configuration/acme/).
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
A typical workflow for using cert-manager with Infisical via ACME consists of the following steps:
|
||||||
|
|
||||||
|
1. Create a [certificate profile](/documentation/platform/pki/certificates/profiles) in Infisical with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on it.
|
||||||
|
2. Install `cert-manager` in your Kubernetes cluster.
|
||||||
|
3. Create a Kubernetes `Secret` containing the EAB (External Account Binding) credentials for the ACME certificate profile.
|
||||||
|
4. Create an `Issuer` or `ClusterIssuer` resource that connects to the desired Infisical [certificate profile](/documentation/platform/pki/certificates/profiles).
|
||||||
|
5. Create a `Certificate` resource defining the certificate you wish to issue and the target `Secret` where the certificate and private key will be stored.
|
||||||
|
6. Use the resulting Kubernetes `Secret` in your Ingresses or other resources.
|
||||||
|
|
||||||
|
## Guide
|
||||||
|
|
||||||
|
The following steps show how to install cert-manager (using `kubectl`) and obtain certificates from Infisical.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create a certificate profile with ACME as the enrollment method in Infisical">
|
||||||
|
|
||||||
|
Follow the instructions [here](/documentation/platform/pki/enrollment-methods/acme) to create a certificate profile that uses ACME enrollment.
|
||||||
|
|
||||||
|
After completion, you will have the following values:
|
||||||
|
- **ACME Directory URL**
|
||||||
|
- **EAB Key ID (KID)**
|
||||||
|
- **EAB Secret**
|
||||||
|
|
||||||
|
These will be needed in later steps.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Currently, the Infisical ACME enrollment method only supports authentication via dedicated EAB credentials generated per certificate profile.
|
||||||
|
|
||||||
|
Support for [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth) is planned for the near future.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Install cert-manager">
|
||||||
|
|
||||||
|
Install cert-manager in your Kubernetes cluster by following the official guide [here](https://cert-manager.io/docs/installation/) or by applying the manifest directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.1/cert-manager.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Create a Kubernetes Secret for the Infisical ACME EAB credentials">
|
||||||
|
Create a Kubernetes `Secret` that contains the **EAB Secret (HMAC key)** obtained in step 1.
|
||||||
|
The cert-manager uses this secret to authenticate with the Infisical ACME server.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="kubectl command">
|
||||||
|
```bash
|
||||||
|
kubectl create secret generic infisical-acme-eab-secret \
|
||||||
|
--namespace <namespace_you_want_to_issue_certificates_in> \
|
||||||
|
--from-literal=eabSecret=<eab_secret>
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Configuration file">
|
||||||
|
```yaml acme-eab-secret.yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: infisical-acme-eab-secret
|
||||||
|
namespace: <namespace_you_want_to_issue_certificates_in>
|
||||||
|
data:
|
||||||
|
eabSecret: <eab_secret>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f acme-eab-secret.yaml
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Create the cert-manager Issuer connecting to Infisical ACME server">
|
||||||
|
Next, create a cert-manager `Issuer` (or `ClusterIssuer`) by replacing the placeholders `<acme_server_url>`, `<your_email>`, and `<acme_eab_kid>` in the configuration below and applying it.
|
||||||
|
This resource configures cert-manager to use your Infisical PKI collection's ACME server for certificate issuance.
|
||||||
|
|
||||||
|
```yaml issuer-infisical.yaml
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Issuer
|
||||||
|
metadata:
|
||||||
|
name: issuer-infisical
|
||||||
|
namespace: <namespace_you_want_to_issue_certificates_in>
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
# ACME server URL from your Infisical certificate profile (Step 1)
|
||||||
|
server: <acme_server_url>
|
||||||
|
# Email address for ACME account
|
||||||
|
# (any valid email works; currently ignored by Infisical)
|
||||||
|
email: <your_email>
|
||||||
|
externalAccountBinding:
|
||||||
|
# EAB Key ID from Step 1
|
||||||
|
keyID: <acme_eab_kid>
|
||||||
|
# Reference to the Kubernetes Secret containing the EAB
|
||||||
|
# HMAC key (created in Step 3)
|
||||||
|
keySecretRef:
|
||||||
|
name: infisical-acme-eab-secret
|
||||||
|
key: eabSecret
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: issuer-infisical-account-key
|
||||||
|
solvers:
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
# Replace with your actual ingress class if different
|
||||||
|
className: nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
kubectl apply -f issuer-infisical.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
You can check that the issuer was created successfully by running the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get issuers.cert-manager.io -n <namespace_of_issuer> -o wide
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NAME AGE
|
||||||
|
issuer-infisical 21h
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
- Currently, the Infisical ACME server only supports the HTTP-01 challenge and requires successful challenge completion before issuing certificates. Support for optional challenges and DNS-01 is planned for a future release.
|
||||||
|
- An `Issuer` is namespace-scoped. Certificates can only be issued using an `Issuer` that exists in the same namespace as the `Certificate` resource.
|
||||||
|
- If you need to issue certificates across multiple namespaces with a single resource, create a `ClusterIssuer` instead. The configuration is identical except `kind: ClusterIssuer` and no `metadata.namespace`.
|
||||||
|
- More details: https://cert-manager.io/docs/configuration/acme/
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Create the Certificate">
|
||||||
|
|
||||||
|
Finally, request a certificate from Infisical ACME server by creating a cert-manager `Certificate` resource.
|
||||||
|
This configuration file specifies the details of the (end-entity/leaf) certificate to be issued.
|
||||||
|
|
||||||
|
```yaml certificate-issuer.yaml
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: certificate-by-issuer
|
||||||
|
namespace: <namespace_you_want_to_issue_certificates_in>
|
||||||
|
spec:
|
||||||
|
dnsNames:
|
||||||
|
- certificate-by-issuer.example.com
|
||||||
|
# name of the resulting Kubernetes Secret
|
||||||
|
secretName: certificate-by-issuer
|
||||||
|
# total validity period of the certificate
|
||||||
|
duration: 48h
|
||||||
|
# cert-manager will attempt renewal 12 hours before expiry
|
||||||
|
renewBefore: 12h
|
||||||
|
privateKey:
|
||||||
|
algorithm: ECDSA
|
||||||
|
# uses NIST P-256 curve
|
||||||
|
size: 256
|
||||||
|
issuerRef:
|
||||||
|
name: issuer-infisical
|
||||||
|
```
|
||||||
|
|
||||||
|
The above sample configuration file specifies a certificate to be issued with the dns name `certificate-by-issuer.example.com` and ECDSA private key using the P-256 curve, valid for 48 hours; the certificate will be automatically renewed by `cert-manager` 12 hours before expiry.
|
||||||
|
The certificate is issued by the issuer `issuer-infisical` created in the previous step and the resulting certificate and private key will be stored in a secret named `certificate-by-issuer`.
|
||||||
|
|
||||||
|
Note that the full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
|
||||||
|
|
||||||
|
You can check that the certificate was created successfully by running the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get certificates -n <namespace_of_your_certificate> -o wide
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NAME READY SECRET ISSUER STATUS AGE
|
||||||
|
certificate-by-issuer True certificate-by-issuer issuer-infisical Certificate is up to date and has not expired 20h
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Use Certificate in Kubernetes Secret">
|
||||||
|
Since the actual certificate and private key are stored in a Kubernetes secret, we can check that the secret was created successfully by running the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get secret certificate-by-issuer -n <namespace_of_your_certificate>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NAME TYPE DATA AGE
|
||||||
|
certificate-by-issuer kubernetes.io/tls 2 26h
|
||||||
|
```
|
||||||
|
|
||||||
|
We can `describe` the secret to get more information about it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl describe secret certificate-by-issuer -n default
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Name: certificate-by-issuer
|
||||||
|
Namespace: default
|
||||||
|
Labels: controller.cert-manager.io/fao=true
|
||||||
|
Annotations: cert-manager.io/alt-names:
|
||||||
|
cert-manager.io/certificate-name: certificate-by-issuer
|
||||||
|
cert-manager.io/common-name:
|
||||||
|
cert-manager.io/alt-names: certificate-by-issuer.example.com
|
||||||
|
cert-manager.io/ip-sans:
|
||||||
|
cert-manager.io/issuer-group: cert-manager.io
|
||||||
|
cert-manager.io/issuer-kind: Issuer
|
||||||
|
cert-manager.io/issuer-name: issuer-infisical
|
||||||
|
cert-manager.io/uri-sans:
|
||||||
|
|
||||||
|
Type: kubernetes.io/tls
|
||||||
|
|
||||||
|
Data
|
||||||
|
====
|
||||||
|
ca.crt: 1306 bytes
|
||||||
|
tls.crt: 2380 bytes
|
||||||
|
tls.key: 227 bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `ca.crt` is the Root CA certificate, `tls.crt` is the requested certificate followed by the certificate chain, and `tls.key` is the private key for the certificate.
|
||||||
|
|
||||||
|
We can decode the certificate and print it out using `openssl`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get secret certificate-by-issuer -n default -o jsonpath='{.data.tls\.crt}' | base64 --decode | openssl x509 -text -noout
|
||||||
|
```
|
||||||
|
|
||||||
|
In any case, the certificate is ready to be used as Kubernetes Secret by your Kubernetes resources.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="What fields can be configured on the Certificate resource?">
|
||||||
|
The full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Currently, not all fields are supported by the Infisical PKI ACME server.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Can certificates be renewed automatically?">
|
||||||
|
Yes. `cert-manager` will automatically renew certificates according to the `renewBefore` threshold of expiry as
|
||||||
|
specified in the corresponding `Certificate` resource.
|
||||||
|
|
||||||
|
You can read more about the `renewBefore` field [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Kubernetes Issuer"
|
|
||||||
description: "Learn how to automatically provision and manage TLS certificates in Kubernetes using Infisical PKI"
|
|
||||||
---
|
|
||||||
|
|
||||||
## Concept
|
|
||||||
|
|
||||||
The Infisical PKI Issuer is an installable Kubernetes [cert-manager](https://cert-manager.io/) controller that uses Infisical PKI to sign certificate requests. The issuer is perfect for getting X.509 certificates for ingresses and other Kubernetes resources and capable of automatically renewing certificates as needed.
|
|
||||||
|
|
||||||
As part of the workflow, you install `cert-manager`, the Infisical PKI Issuer, and configure resources to represent the connection details to your Infisical PKI and the certificates you wish to issue. Each issued certificate and corresponding private key is made available in a Kubernetes secret.
|
|
||||||
|
|
||||||
We recommend reading the [cert-manager documentation](https://cert-manager.io/docs/) for a fuller understanding of all the moving parts.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
A typical workflow for using the Infisical PKI Issuer to issue certificates for your Kubernetes resources consists of the following steps:
|
|
||||||
|
|
||||||
1. Creating a machine identity in Infisical.
|
|
||||||
2. Creating a Kubernetes secret to store the credentials of the machine identity.
|
|
||||||
3. Installing `cert-manager` into your Kubernetes cluster.
|
|
||||||
4. Installing the Infisical PKI Issuer controller into your Kubernetes cluster.
|
|
||||||
5. Creating an `Issuer` or `ClusterIssuer` resource in your Kubernetes cluster to represent the Infisical PKI issuer you wish to use.
|
|
||||||
6. Create the approver policy to accept certificate request.
|
|
||||||
7. Creating a `Certificate` resource in your Kubernetes cluster to represent a certificate you wish to issue. As part of this step, you specify the Kubernetes `Secret` to create and store the issued certificate and private key.
|
|
||||||
8. Consuming the issued certificate across your Kubernetes resources from the specified Kubernetes `Secret`.
|
|
||||||
|
|
||||||
## Guide
|
|
||||||
|
|
||||||
In the following steps, we explore how to install the Infisical PKI Issuer using [kubectl](https://github.com/kubernetes/kubectl) and use it to obtain certificates for your Kubernetes resources.
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
<Step title="Create an identity in Infisical">
|
|
||||||
|
|
||||||
Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth.
|
|
||||||
|
|
||||||
By the end of this step, you should have a **Client ID** and **Client Secret** on hand as part of the Universal Auth configuration for the Infisical PKI Issuer to authenticate with Infisical; this will be useful in steps 4 and 5.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Currently, the Infisical PKI Issuer only supports authenticating with Infisical via the [Universal Auth](/documentation/platform/identities/universal-auth) authentication method.
|
|
||||||
|
|
||||||
We're planning to add support for [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth) in the near future.
|
|
||||||
</Note>
|
|
||||||
</Step>
|
|
||||||
<Step title="Install cert-manager">
|
|
||||||
Install `cert-manager` into your Kubernetes cluster by following the instructions [here](https://cert-manager.io/docs/installation/) or by running the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
<Step title="Install the Issuer Controller">
|
|
||||||
Install the Infisical PKI Issuer controller into your Kubernetes cluster using one of the following methods:
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<Tab title="Helm">
|
|
||||||
```bash
|
|
||||||
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
|
||||||
helm install infisical-pki-issuer infisical-helm-charts/infisical-pki-issuer
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
<Tab title="kubectl">
|
|
||||||
```bash
|
|
||||||
kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</Step>
|
|
||||||
<Step title="Create Kubernetes Secret for Infisical PKI Issuer">
|
|
||||||
Start by creating a Kubernetes `Secret` containing the **Client Secret** from step 1. As mentioned previously, this will be used by the Infisical PKI issuer to authenticate with Infisical.
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<Tab title="kubectl command">
|
|
||||||
```bash
|
|
||||||
kubectl create secret generic issuer-infisical-client-secret \
|
|
||||||
--namespace <namespace_you_want_to_issue_certificates_in> \
|
|
||||||
--from-literal=clientSecret=<client_secret>
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Configuration file">
|
|
||||||
```yaml secret-issuer.yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: issuer-infisical-client-secret
|
|
||||||
namespace: <namespace_you_want_to_issue_certificates_in>
|
|
||||||
data:
|
|
||||||
clientSecret: <client_secret>
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl apply -f secret-issuer.yaml
|
|
||||||
```
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</Step>
|
|
||||||
<Step title="Create Infisical PKI Issuer">
|
|
||||||
Next, create the Infisical PKI Issuer by filling out `url`, `clientId`, `projectId` or `certificateTemplateName`, and applying the following configuration file for the `Issuer` resource.
|
|
||||||
This configuration file specifies the connection details to your Infisical PKI CA to be used for issuing certificates.
|
|
||||||
|
|
||||||
```yaml infisical-issuer.yaml
|
|
||||||
apiVersion: infisical-issuer.infisical.com/v1alpha1
|
|
||||||
kind: Issuer
|
|
||||||
metadata:
|
|
||||||
name: issuer-infisical
|
|
||||||
namespace: <namespace_you_want_to_issue_certificates_in>
|
|
||||||
spec:
|
|
||||||
url: "https://app.infisical.com" # the URL of your Infisical instance
|
|
||||||
projectId: <project_id> # the ID of the project you want to use to issue certificates
|
|
||||||
certificateTemplateName: <certificate_template_name> # the name of the certificate template you want to use to issue certificates against
|
|
||||||
authentication:
|
|
||||||
universalAuth:
|
|
||||||
clientId: <client_id> # the Client ID from step 1
|
|
||||||
secretRef: # reference to the Secret created in step 4
|
|
||||||
name: "issuer-infisical-client-secret"
|
|
||||||
key: "clientSecret"
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
kubectl apply -f infisical-issuer.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
You can check that the issuer was created successfully by running the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl get issuers.infisical-issuer.infisical.com -n <namespace_of_issuer> -o wide
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
NAME AGE
|
|
||||||
issuer-infisical 21h
|
|
||||||
```
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
An `Issuer` is a namespaced resource, and it is not possible to issue certificates from an `Issuer` in a different namespace.
|
|
||||||
This means you will need to create an `Issuer` in each namespace you wish to obtain `Certificates` in.
|
|
||||||
|
|
||||||
If you want to create a single `Issuer` that can be consumed in multiple namespaces, you should consider creating a `ClusterIssuer` resource. This is almost identical to the `Issuer` resource, however is non-namespaced so it can be used to issue `Certificates` across all namespaces.
|
|
||||||
|
|
||||||
You can read more about the `Issuer` and `ClusterIssuer` resources [here](https://cert-manager.io/docs/configuration/).
|
|
||||||
</Note>
|
|
||||||
</Step>
|
|
||||||
<Step title="Create Approver Policy">
|
|
||||||
If you create a `CertificateRequest` now, you'll notice it's neither approved nor denied. This is expected because by default cert-manager approver controller requires an approver-policy.
|
|
||||||
|
|
||||||
To enable approval, create the following YAML file and apply it:
|
|
||||||
|
|
||||||
```yaml infisical-approver-policy.yaml
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRole
|
|
||||||
metadata:
|
|
||||||
name: infisical-issuer-approver
|
|
||||||
rules:
|
|
||||||
# Permission to approve or deny CertificateRequests for signers in cert-manager.io API group
|
|
||||||
- apiGroups: ['cert-manager.io']
|
|
||||||
resources: ['signers']
|
|
||||||
verbs: ['approve']
|
|
||||||
resourceNames:
|
|
||||||
# Grant approval permissions for namespaced issuers
|
|
||||||
- "issuers.infisical-issuer.infisical.com/default.issuer-infisical"
|
|
||||||
# Grant approval permissions for cluster-scoped issuers
|
|
||||||
- "clusterissuers.infisical-issuer.infisical.com/clusterissuer-infisical"
|
|
||||||
---
|
|
||||||
# Bind the cert-manager service account to the new role
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: ClusterRoleBinding
|
|
||||||
metadata:
|
|
||||||
name: infisical-issuer-approver-binding
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: cert-manager
|
|
||||||
namespace: cert-manager
|
|
||||||
roleRef:
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
kind: ClusterRole
|
|
||||||
name: infisical-issuer-approver
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
kubectl apply -f infisical-approver-policy.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
This configuration creates a `ClusterRole` named `infisical-issuer-approver` that grants approval permissions for specific Infisical issuer types. It then binds this role to the cert-manager service account, allowing it to approve certificate requests from your Infisical issuers.
|
|
||||||
|
|
||||||
For information, check out [cert manager approval policy doc](https://cert-manager.io/docs/policy/approval/approver-policy/).
|
|
||||||
</Step>
|
|
||||||
<Step title="Create Certificate">
|
|
||||||
|
|
||||||
Finally, create a `Certificate` by applying the following configuration file.
|
|
||||||
This configuration file specifies the details of the (end-entity/leaf) certificate to be issued.
|
|
||||||
|
|
||||||
```yaml certificate-issuer.yaml
|
|
||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: Certificate
|
|
||||||
metadata:
|
|
||||||
name: certificate-by-issuer
|
|
||||||
namespace: <namespace_you_want_to_issue_certificates_in>
|
|
||||||
spec:
|
|
||||||
commonName: certificate-by-issuer.example.com # the common name for the certificate
|
|
||||||
secretName: certificate-by-issuer # the name of the Kubernetes Secret to create and store the certificate and private key in
|
|
||||||
issuerRef:
|
|
||||||
name: issuer-infisical
|
|
||||||
group: infisical-issuer.infisical.com
|
|
||||||
kind: Issuer
|
|
||||||
privateKey: # the algorithm and key size to use
|
|
||||||
algorithm: ECDSA
|
|
||||||
size: 256
|
|
||||||
duration: 48h # the ttl for the certificate
|
|
||||||
renewBefore: 12h # the time before the certificate expiry that the certificate should be automatically renewed
|
|
||||||
```
|
|
||||||
|
|
||||||
The above sample configuration file specifies a certificate to be issued with the common name `certificate-by-issuer.example.com` and ECDSA private key using the P-256 curve, valid for 48 hours; the certificate will be automatically renewed by `cert-manager` 12 hours before expiry.
|
|
||||||
The certificate is issued by the issuer `issuer-infisical` created in the previous step and the resulting certificate and private key will be stored in a secret named `certificate-by-issuer`.
|
|
||||||
|
|
||||||
Note that the full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
|
|
||||||
|
|
||||||
You can check that the certificate was created successfully by running the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl get certificates -n <namespace_of_your_certificate> -o wide
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
NAME READY SECRET ISSUER STATUS AGE
|
|
||||||
certificate-by-issuer True certificate-by-issuer issuer-infisical Certificate is up to date and has not expired 20h
|
|
||||||
```
|
|
||||||
</Step>
|
|
||||||
<Step title="Use Certificate in Kubernetes Secret">
|
|
||||||
Since the actual certificate and private key are stored in a Kubernetes secret, we can check that the secret was created successfully by running the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl get secret certificate-by-issuer -n <namespace_of_your_certificate>
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
NAME TYPE DATA AGE
|
|
||||||
certificate-by-issuer kubernetes.io/tls 2 26h
|
|
||||||
```
|
|
||||||
|
|
||||||
We can `describe` the secret to get more information about it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl describe secret certificate-by-issuer -n default
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
Name: certificate-by-issuer
|
|
||||||
Namespace: default
|
|
||||||
Labels: controller.cert-manager.io/fao=true
|
|
||||||
Annotations: cert-manager.io/alt-names:
|
|
||||||
cert-manager.io/certificate-name: certificate-by-issuer
|
|
||||||
cert-manager.io/common-name: certificate-by-issuer.example.com
|
|
||||||
cert-manager.io/ip-sans:
|
|
||||||
cert-manager.io/issuer-group: infisical-issuer.infisical.com
|
|
||||||
cert-manager.io/issuer-kind: Issuer
|
|
||||||
cert-manager.io/issuer-name: issuer-infisical
|
|
||||||
cert-manager.io/uri-sans:
|
|
||||||
|
|
||||||
Type: kubernetes.io/tls
|
|
||||||
|
|
||||||
Data
|
|
||||||
====
|
|
||||||
ca.crt: 1306 bytes
|
|
||||||
tls.crt: 2380 bytes
|
|
||||||
tls.key: 227 bytes
|
|
||||||
```
|
|
||||||
|
|
||||||
Here, `ca.crt` is the Root CA certificate, `tls.crt` is the requested certificate followed by the certificate chain, and `tls.key` is the private key for the certificate.
|
|
||||||
|
|
||||||
We can decode the certificate and print it out using `openssl`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl get secret certificate-by-issuer -n default -o jsonpath='{.data.tls\.crt}' | base64 --decode | openssl x509 -text -noout
|
|
||||||
```
|
|
||||||
|
|
||||||
In any case, the certificate is ready to be used as Kubernetes Secret by your Kubernetes resources.
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
</Steps>
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
<AccordionGroup>
|
|
||||||
<Accordion title="What fields can be configured on the Certificate resource?">
|
|
||||||
The full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Currently, not all fields are supported by the Infisical PKI Issuer.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
</Accordion>
|
|
||||||
<Accordion title="Can certificates be renewed automatically?">
|
|
||||||
Yes. `cert-manager` will automatically renew certificates according to the `renewBefore` threshold of expiry as
|
|
||||||
specified in the corresponding `Certificate` resource.
|
|
||||||
|
|
||||||
You can read more about the `renewBefore` field [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
|
|
||||||
|
|
||||||
</Accordion>
|
|
||||||
<Accordion title="Why is my CertificateRequest not being approved, showing 'CertificateRequest has not been approved yet. Ignoring.'?">
|
|
||||||
If you see log messages similar to:
|
|
||||||
```
|
|
||||||
"CertificateRequest has not been approved yet. Ignoring.","controller":"certificaterequest","controllerGroup":"cert-manager.io","controllerKind":"CertificateRequest","CertificateRequest":{"name":"skynet-infisical-rta-rsa2048-1","namespace":"infisical-system"},"namespace":"infisical-system","name":"skynet-infisical-rta-rsa2048-1","reconcileID":"bfb7cad9-d867-45b5-b3a3-0139e731b7a6"}
|
|
||||||
```
|
|
||||||
This indicates that the `CertificateRequest` has been created, but `cert-manager` has not yet approved it. This typically occurs because a necessary approver policy is missing. Refer to the documentation above to create an approver policy.
|
|
||||||
</Accordion>
|
|
||||||
</AccordionGroup>
|
|
||||||
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 407 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 358 KiB After Width: | Height: | Size: 368 KiB |
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
title: "DNS Made Easy"
|
||||||
|
description: "Learn how to configure a DNS Made Easy Connection for Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
Infisical supports connecting to DNS Made Easy using API key and secret key for secure access to your DNS Made Easy service.
|
||||||
|
|
||||||
|
## Configure API key and secret Key for Infisical
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Generate API key and secret key">
|
||||||
|
Navigate to your DNS Made Easy dashboard and go to **Account Information** under the **Config** top menu.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If your **API Key** and **Secret Key** are already available, proceed to step 2.
|
||||||
|
|
||||||
|
Otherwise, check the **Generate New API Credentials** then click the **Save** button to generate the new API credentials.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Copy Your API Key and Secret Key">
|
||||||
|
After creation, copy your API key and secret key.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Keep your API key and secret key secure and do not share it.
|
||||||
|
Anyone with access to this token can manage your DNS Made Easy resources.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Setup DNS Made Easy Connection in Infisical
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to App Connections">
|
||||||
|
Navigate to the **App Connections** page in the desired project. 
|
||||||
|
</Step>
|
||||||
|
<Step title="Add Connection">
|
||||||
|
Select the **DNS Made Easy Connection** option from the connection options
|
||||||
|
modal. 
|
||||||
|
</Step>
|
||||||
|
<Step title="Input Credentials">
|
||||||
|
Enter your DNS Made Easy API key and secret key in the provided fields and
|
||||||
|
click **Connect to DNS Made Easy** to establish the connection. 
|
||||||
|
</Step>
|
||||||
|
<Step title="Connection Created">
|
||||||
|
Your **DNS Made Easy Connection** is now available for use in your Infisical
|
||||||
|
projects. 
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
@@ -50,6 +50,11 @@ Prerequisites:
|
|||||||

|
team](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png)
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
If you need to sync only one space, assign the service account to that space. Otherwise, allow access to all spaces for multi-space syncs.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
|
||||||
On the **Members** tab, click on the **Add Member** button, add your **Infisical Service Account** and click on the **Add** button.
|
On the **Members** tab, click on the **Add Member** button, add your **Infisical Service Account** and click on the **Add** button.
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
title: "AWS Lambda"
|
||||||
|
sidebarTitle: "AWS Lambda"
|
||||||
|
description: "How to use Infisical secrets in AWS Lambda"
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn how to sync Infisical secrets to AWS Lambda regardless of how you deploy your function. This guide covers the following strategies:
|
||||||
|
|
||||||
|
- Infisical SDKs
|
||||||
|
- AWS Secrets Manager integration
|
||||||
|
- AWS Systems Manager Parameter Store integration
|
||||||
|
- AWS CLI
|
||||||
|
|
||||||
|
## Choose your sync strategy
|
||||||
|
|
||||||
|
### 1. Fetch secrets at runtime with Infisical SDKs
|
||||||
|
|
||||||
|
If you control the Lambda code, the simplest method is to fetch secrets directly from Infisical using one of our SDKs.
|
||||||
|
You can read more about the Infisical SDKs [here](/sdks/overview).
|
||||||
|
|
||||||
|
### 2. Push via secret sync
|
||||||
|
|
||||||
|
Configure a secret sync from your Infisical project, and Infisical will keep your Secrets Manager or Parameter Store values up to date. Your Lambda function can then reference those secrets directly.
|
||||||
|
Learn more about the [AWS Secrets Manager integration](/integrations/secret-syncs/aws-secrets-manager) and the [AWS Parameter Store integration](/integrations/secret-syncs/aws-parameter-store).
|
||||||
|
|
||||||
|
### 3. Push environment variables directly using the AWS CLI
|
||||||
|
|
||||||
|
For straightforward workflows or quick rotations, you can push Infisical secrets directly into Lambda environment variables using the AWS CLI.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- AWS CLI v2 installed and authenticated
|
||||||
|
- `jq` installed locally
|
||||||
|
- An IAM principal with `lambda:UpdateFunctionConfiguration`
|
||||||
|
- Infisical CLI (`infisical`) configured
|
||||||
|
|
||||||
|
### IAM permissions
|
||||||
|
|
||||||
|
Attach a policy like the one below to the IAM user or role responsible for updating Lambda configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "LambdaConfig",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["lambda:UpdateFunctionConfiguration"],
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
{" "}
|
||||||
|
Replacing Lambda environment variables using the AWS CLI overwrites the entire
|
||||||
|
`Variables` object. Make sure to export your current values so you can import them
|
||||||
|
into Infisical.{" "}
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
#### Push secrets to Lambda
|
||||||
|
|
||||||
|
Use the Infisical CLI to export secrets as JSON and pass them to the AWS CLI.
|
||||||
|
The example below targets a project by ID, but you can also use the `--project` and `--env` flags.
|
||||||
|
Learn more about `infisical export` [here](/cli/commands/export#infisical-export).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
FUNCTION_NAME=infisical-env-test
|
||||||
|
REGION=us-east-1
|
||||||
|
PROJECT_ID=1234567890
|
||||||
|
|
||||||
|
aws lambda update-function-configuration \
|
||||||
|
--function-name "$FUNCTION_NAME" \
|
||||||
|
--region "$REGION" \
|
||||||
|
--environment "$(
|
||||||
|
infisical export \
|
||||||
|
--format=json \
|
||||||
|
--projectId="$PROJECT_ID" \
|
||||||
|
| jq 'map({(.key): .value}) | add | {Variables: .}'
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
On success, the updated `Environment.Variables` block will be returned.
|
||||||
|
Verify the values in the Lambda console or by invoking the function.
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Automate this step in CI/CD. Run `infisical export` using an Infisical Token
|
||||||
|
scoped to your project and environment, and trigger the sync as part of your
|
||||||
|
deployment workflow. Learn more about the [Infisical
|
||||||
|
Token](/cli/commands/export#infisical-export:infisical-token).
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
We recommend using automatic secret syncs to AWS Secrets Manager or AWS
|
||||||
|
Parameter Store to keep your secrets continuously in sync and avoid manually
|
||||||
|
updating the Lambda configuration.
|
||||||
|
</Note>
|
||||||
@@ -8,12 +8,12 @@ It eliminates the need to modify application logic by enabling clients to decide
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Key features:
|
## Key Features
|
||||||
|
|
||||||
- Token renewal: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume
|
- **Token lifecycle management**: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume
|
||||||
- Templating: Renders secrets via user provided templates to desired formats for applications to consume
|
- **Templating**: Renders secrets and dynamic secret leases via user provided templates to desired formats for applications to consume
|
||||||
|
|
||||||
### Token renewal
|
## Token Renewal
|
||||||
|
|
||||||
The Infisical agent can help manage the life cycle of access tokens. The token renewal process is split into two main components: a `Method`, which is the authentication process suitable for your current setup, and `Sinks`, which are the places where the agent deposits the new access token whenever it receives updates.
|
The Infisical agent can help manage the life cycle of access tokens. The token renewal process is split into two main components: a `Method`, which is the authentication process suitable for your current setup, and `Sinks`, which are the places where the agent deposits the new access token whenever it receives updates.
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ Every time the agent successfully retrieves a new access token, it writes the ne
|
|||||||
to retrieve secrets from Infisical
|
to retrieve secrets from Infisical
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
### Templating
|
## Templating
|
||||||
|
|
||||||
The Infisical agent can help deliver formatted secrets to your application in a variety of environments. To achieve this, the agent will retrieve secrets from Infisical, format them using a specified template, and then save these formatted secrets to a designated file path.
|
The Infisical agent can help deliver formatted secrets to your application in a variety of environments. To achieve this, the agent will retrieve secrets from Infisical, format them using a specified template, and then save these formatted secrets to a designated file path.
|
||||||
|
|
||||||
@@ -40,16 +40,185 @@ If this initial attempt is unsuccessful, the agent will momentarily pauses befor
|
|||||||
Once the agent successfully obtains a valid access token, the agent proceeds to fetch the secrets from Infisical using it.
|
Once the agent successfully obtains a valid access token, the agent proceeds to fetch the secrets from Infisical using it.
|
||||||
It then formats these secrets using the user provided templates and writes the formatted data to configured file paths.
|
It then formats these secrets using the user provided templates and writes the formatted data to configured file paths.
|
||||||
|
|
||||||
|
|
||||||
|
### Available secret template functions
|
||||||
|
|
||||||
|
The secret template functions is what you will use to fetch resources such as static secrets and dynamic secret leases from Infisical. Below is a list of the available secret template functions that you can use in your templates.
|
||||||
|
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="secret">
|
||||||
|
```bash
|
||||||
|
secret "<project-id>" "environment-slug" "<secret-path>" "<optional-modifier>"
|
||||||
|
```
|
||||||
|
```bash example-template-usage-1
|
||||||
|
{{- with secret "6553ccb2b7da580d7f6e7260" "dev" "/" `{"recursive": false, "expandSecretReferences": true}` }}
|
||||||
|
{{- range . }}
|
||||||
|
{{ .Key }}={{ .Value }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
```
|
||||||
|
```bash example-template-usage-2
|
||||||
|
{{- with secret "da8056c8-01e2-4d24-b39f-cb4e004b8d44" "staging" "/" `{"recursive": true, "expandSecretReferences": true}` }}
|
||||||
|
{{- range . }}
|
||||||
|
{{- if eq .SecretPath "/"}}
|
||||||
|
{{ .Key }}={{ .Value }}
|
||||||
|
{{- else}}
|
||||||
|
{{ .SecretPath }}/{{ .Key }}={{ .Value }}
|
||||||
|
{{- end}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Function name**: `secret`
|
||||||
|
|
||||||
|
**Description**: This function can be used to render the full list of secrets within a given project, environment and secret path.
|
||||||
|
|
||||||
|
An optional JSON argument is also available. It includes the properties `recursive`, which defaults to false, and `expandSecretReferences`, which defaults to true and expands the returned secrets.
|
||||||
|
|
||||||
|
|
||||||
|
**Returns**: A single secret object with the following keys `Key, WorkspaceId, Value, SecretPath, Type, ID, and Comment`
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="getSecretByName">
|
||||||
|
```bash
|
||||||
|
getSecretByName "<project-id>" "<environment-slug>" "<secret-path>" "<secret-name>"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash example-template-usage
|
||||||
|
{{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}}
|
||||||
|
{{ if .Value }}
|
||||||
|
password = "{{ .Value }}"
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Function name**: `getSecretByName`
|
||||||
|
|
||||||
|
**Description**: This function can be used to render a single secret by it's name.
|
||||||
|
|
||||||
|
**Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment`
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="dynamic_secret">
|
||||||
|
```bash
|
||||||
|
dynamic_secret "<project-slug>" "<environment-slug>" "<secret-path>" "<dynamic-secret-name>" "<lease-ttl>"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash example-redis-dynamic-secret
|
||||||
|
{{ with dynamic_secret "aaa-o7en-s5qm" "dev" "/" "redis" "1m" }}
|
||||||
|
{{ .DB_USERNAME }}={{ .DB_PASSWORD }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**Function Name**: `dynamic_secret`
|
||||||
|
|
||||||
|
**Description**: This function can be used to render a dynamic secret lease credentials. The credentials are automatically renewed before they expire, ensuring that the rendered credentials are always up-to-date.
|
||||||
|
|
||||||
|
**Returns**: An object with keys corresponding to the dynamic secret lease credentials.
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Note that if you have multiple dynamic secret templates with identical configurations, only one lease will be created in Infisical for those templates, and the same lease will be written to your specified destination paths.
|
||||||
|
</Tip>
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
The Infisical Agent supports clientside caching of Dynamic Secret leases. If the cache is enabled, the agent will persist the dynamic secret leases to the cache across restarts of the agent.
|
||||||
|
|
||||||
|
### Persistent Caching
|
||||||
|
|
||||||
|
The Agent currently only supports persistent caching. To utilize persistent caching, you must be within a Kubernetes environment. We recommend using the [Infisical Agent Injector](/integrations/platforms/kubernetes-injector) to inject the agent into pods within your Kubernetes cluster on demand.
|
||||||
|
|
||||||
|
### Cache eviction
|
||||||
|
|
||||||
|
Cache eviction is the process of removing cached data from the cache. The Agent will automatically evict cached data when the cache is full during a garbage collection cycle which is triggered every 10 minutes.
|
||||||
|
|
||||||
|
The cache will also automatically evict cached data that has gone stale or is about to go stale. For dynamic resources (such as dynamic secret leases), there's a TTL (Time-to-Live) associated with each lease which is used to determine if the lease is stale or about to go stale.
|
||||||
|
If a stale dynamic secret lease is detected, it will be automatically evicted from the cache and replaced with a new up-to-date lease.
|
||||||
|
|
||||||
|
|
||||||
|
### Cache Configuration
|
||||||
|
|
||||||
|
Configuring the cache is done through the agent configuration file. The following fields are available to configure the cache:
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Persistent Caching">
|
||||||
|
<ParamField query="cache.persistent.type" type="string">
|
||||||
|
The type of persistent caching to use. Currently only `kubernetes` is available, and will only work within Kubernetes environments.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="cache.persistent.path" type="string">
|
||||||
|
The path to where your persistent cache will be stored.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Persistent caching is only supported within kubernetes environments at the moment. Please refer to the [Infisical Agent Injector](/integrations/platforms/kubernetes-injector) documentation for more information on how to use persistent caching within Kubernetes environments.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
```yaml example-agent-config-file.yaml
|
||||||
|
cache:
|
||||||
|
persistent:
|
||||||
|
type: "kubernetes"
|
||||||
|
path: "/home/infisical/cache"
|
||||||
|
service-account-token-path: "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
|
||||||
|
|
||||||
|
## Retrying mechanism
|
||||||
|
|
||||||
|
The agent will automatically attempt to retry failed API requests such as authentication, secrets retrieval, dynamic secret lease provisioning, etc.
|
||||||
|
By default, the agent will retry up to 3 times with a base delay of 200ms and a maximum delay of 5s.
|
||||||
|
|
||||||
|
You can configure the retrying mechanism through the agent configuration file. The following fields are available to configure the retrying mechanism:
|
||||||
|
|
||||||
|
|
||||||
|
<ParamField query="infisical.retry-strategy.max-retries" type="number">
|
||||||
|
How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="infisical.retry-strategy.max-delay" type="duration">
|
||||||
|
The maximum delay between retries. Defaults to `5s` (5 seconds).
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="infisical.retry-strategy.base-delay" type="duration">
|
||||||
|
The base delay between retries. Defaults to `200ms` (200 milliseconds).
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
```yaml example-agent-config-file.yaml
|
||||||
|
infisical:
|
||||||
|
address: "https://app.infisical.com"
|
||||||
|
retry-strategy:
|
||||||
|
max-retries: 3
|
||||||
|
max-delay: "5s"
|
||||||
|
base-delay: "200ms"
|
||||||
|
|
||||||
|
# ... rest of the agent configuration file
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Agent configuration file
|
## Agent configuration file
|
||||||
|
|
||||||
To set up the authentication method for token renewal and to define secret templates, the Infisical agent requires a YAML configuration file containing properties defined below.
|
To set up the authentication method for token renewal and to define secret templates, the Infisical agent requires a YAML configuration file containing properties defined below.
|
||||||
While specifying an authentication method is mandatory to start the agent, configuring sinks and secret templates are optional.
|
While specifying an authentication method is mandatory to start the agent, configuring sinks and secret templates are optional.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. |
|
| `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. |
|
||||||
| `infisical.exit-after-auth` | Whether to exit the agent after authentication and first secret render. Default: `"false"`. |
|
| `infisical.exit-after-auth` | Whether to exit the agent after authentication and first secret render. Default: `"false"`. |
|
||||||
| `infisical.revoke-credentials-on-shutdown` | Whether to revoke all managed dynamic secret leases and identity access tokens on shutdown. Default: `"false"`. |
|
| `infisical.revoke-credentials-on-shutdown` | Whether to revoke all managed dynamic secret leases and identity access tokens on shutdown. Default: `"false"`. |
|
||||||
|
| `infisical.retry-strategy.max-retries` | How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. |
|
||||||
|
| `infisical.retry-strategy.max-delay` | The maximum delay between retries. Defaults to `5s` (5 seconds). |
|
||||||
|
| `infisical.retry-strategy.base-delay` | The base delay between retries. Defaults to `200ms` (200 milliseconds). |
|
||||||
| `auth.type` | The type of authentication method used. Available options: `universal-auth`, `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, `aws-iam` |
|
| `auth.type` | The type of authentication method used. Available options: `universal-auth`, `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, `aws-iam` |
|
||||||
| `auth.config.identity-id` | The file path where the machine identity id is stored<br/><br/>This field is required when using any of the following auth types: `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. |
|
| `auth.config.identity-id` | The file path where the machine identity id is stored<br/><br/>This field is required when using any of the following auth types: `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. |
|
||||||
| `auth.config.service-account-token` | Path to the Kubernetes service account token to use (optional)<br/><br/>Default: `/var/run/secrets/kubernetes.io/serviceaccount/token` |
|
| `auth.config.service-account-token` | Path to the Kubernetes service account token to use (optional)<br/><br/>Default: `/var/run/secrets/kubernetes.io/serviceaccount/token` |
|
||||||
@@ -59,10 +228,13 @@ While specifying an authentication method is mandatory to start the agent, confi
|
|||||||
| `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. |
|
| `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. |
|
||||||
| `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. |
|
| `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. |
|
||||||
| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. |
|
| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. |
|
||||||
|
| `cache.persistent.type` | The type of persistent caching to use. Currently only `kubernetes` is available, and will only work within Kubernetes environments. |
|
||||||
|
| `cache.persistent.path` | The path to where your persistent cache will be stored. |
|
||||||
|
| `cache.persistent.service-account-token-path` | The path to the Kubernetes service account token to use for encrypting the persistent cache. Required when using `kubernetes` cache type. Defaults to `/var/run/secrets/kubernetes.io/serviceaccount/token` |
|
||||||
| `templates[].source-path` | The path to the template file that should be used to render secrets. |
|
| `templates[].source-path` | The path to the template file that should be used to render secrets. |
|
||||||
| `templates[].template-content` | The inline secret template to be used for rendering the secrets. |
|
| `templates[].template-content` | The inline secret template to be used for rendering the secrets. |
|
||||||
| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. |
|
| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. |
|
||||||
| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5 minutes` (optional) |
|
| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5m` (5 minutes) (optional) |
|
||||||
| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) |
|
| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) |
|
||||||
| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) |
|
| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) |
|
||||||
|
|
||||||
@@ -309,80 +481,3 @@ After defining the agent configuration file, run the command below pointing to t
|
|||||||
```bash
|
```bash
|
||||||
infisical agent --config example-agent-config-file.yaml
|
infisical agent --config example-agent-config-file.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available secret template functions
|
|
||||||
|
|
||||||
<Accordion title="listSecrets">
|
|
||||||
```bash
|
|
||||||
listSecrets "<project-id>" "environment-slug" "<secret-path>" "<optional-modifier>"
|
|
||||||
```
|
|
||||||
```bash example-template-usage-1
|
|
||||||
{{- with listSecrets "6553ccb2b7da580d7f6e7260" "dev" "/" `{"recursive": false, "expandSecretReferences": true}` }}
|
|
||||||
{{- range . }}
|
|
||||||
{{ .Key }}={{ .Value }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
```
|
|
||||||
```bash example-template-usage-2
|
|
||||||
{{- with secret "da8056c8-01e2-4d24-b39f-cb4e004b8d44" "staging" "/" `{"recursive": true, "expandSecretReferences": true}` }}
|
|
||||||
{{- range . }}
|
|
||||||
{{- if eq .SecretPath "/"}}
|
|
||||||
{{ .Key }}={{ .Value }}
|
|
||||||
{{- else}}
|
|
||||||
{{ .SecretPath }}/{{ .Key }}={{ .Value }}
|
|
||||||
{{- end}}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Function name**: listSecrets
|
|
||||||
|
|
||||||
**Description**: This function can be used to render the full list of secrets within a given project, environment and secret path.
|
|
||||||
|
|
||||||
An optional JSON argument is also available. It includes the properties `recursive`, which defaults to false, and `expandSecretReferences`, which defaults to true and expands the returned secrets.
|
|
||||||
|
|
||||||
|
|
||||||
**Returns**: A single secret object with the following keys `Key, WorkspaceId, Value, SecretPath, Type, ID, and Comment`
|
|
||||||
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="getSecretByName">
|
|
||||||
```bash
|
|
||||||
getSecretByName "<project-id>" "<environment-slug>" "<secret-path>" "<secret-name>"
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash example-template-usage
|
|
||||||
{{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}}
|
|
||||||
{{ if .Value }}
|
|
||||||
password = "{{ .Value }}"
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Function name**: getSecretByName
|
|
||||||
|
|
||||||
**Description**: This function can be used to render a single secret by it's name.
|
|
||||||
|
|
||||||
**Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment`
|
|
||||||
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="dynamic_secret">
|
|
||||||
```bash
|
|
||||||
dynamic_secret "<project-slug>" "<environment-slug>" "<secret-path>" "<dynamic-secret-name>" "<lease-ttl>"
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash example-redis-dynamic-secret
|
|
||||||
{{ with dynamic_secret "aaa-o7en-s5qm" "dev" "/" "redis" "1m" }}
|
|
||||||
{{ .DB_USERNAME }}={{ .DB_PASSWORD }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
**Function Name**: dynamic_secret
|
|
||||||
|
|
||||||
**Description**: This function can be used to render a dynamic secret lease credentials. The credentials are automatically renewed before they expire, ensuring that the rendered credentials are always up-to-date.
|
|
||||||
|
|
||||||
**Returns**: An object with keys corresponding to the dynamic secret lease credentials.
|
|
||||||
```
|
|
||||||
</Accordion>
|
|
||||||
@@ -120,6 +120,7 @@ You will need to set the `nodeSelector.kubernetes.io/os` label to `windows` and
|
|||||||
|
|
||||||
The Infisical Agent Injector supports the following annotations:
|
The Infisical Agent Injector supports the following annotations:
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
<Accordion title="org.infisical.com/inject">
|
<Accordion title="org.infisical.com/inject">
|
||||||
The inject annotation is used to enable the injector on a pod. Set the value to `true` and the pod will be patched with an Infisical Agent container on update or create.
|
The inject annotation is used to enable the injector on a pod. Set the value to `true` and the pod will be patched with an Infisical Agent container on update or create.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
@@ -134,6 +135,69 @@ The Infisical Agent Injector supports the following annotations:
|
|||||||
The agent config map annotation is used to specify the name of the config map that contains the configuration for the injector. The config map must be in the same namespace as the pod.
|
The agent config map annotation is used to specify the name of the config map that contains the configuration for the injector. The config map must be in the same namespace as the pod.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-cache-enabled">
|
||||||
|
Whether to enable client-side caching of dynamic secret leases. Defaults to `false`. If you set this to `true`, the agent will persist any dynamic secret leases across restarts of the agent. This is especially useful when using the `sidecar-init` inject mode, to pass the dynamic secret leases created in the init container to the sidecar container.
|
||||||
|
This will ensure that no new leases are created except those initially created in the init container. The sidecar container will register the leases created in the init container and start managing them from that point onwards.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-revoke-on-shutdown">
|
||||||
|
Whether to revoke all managed dynamic secret leases and machine identity access tokens on shutdown. Defaults to `false`.
|
||||||
|
|
||||||
|
If you set this to `true`, all managed dynamic secret leases and machine identity access tokens will be revoked when a `SIGTERM` signal is sent to the agents container _(such as when a pod is terminated or when the pod is restarted)_.
|
||||||
|
|
||||||
|
**Note:** In disaster events such as cluster power outages, a `SIGTERM` signal won't be sent to the agents container, and the credentials will not be revoked.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-client-max-retries">
|
||||||
|
How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-client-max-delay">
|
||||||
|
The maximum delay between retries. Defaults to `5s` (5 seconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-client-base-delay">
|
||||||
|
The base delay between retries. Defaults to `200ms` (200 milliseconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-limits-cpu">
|
||||||
|
The maximum CPU limit for the agent containers.
|
||||||
|
|
||||||
|
Linux Pods: Defaults to `500m` (500 milliCPUs).
|
||||||
|
Windows Pods: Defaults to `500m` (500 milliCPUs).
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-requests-cpu">
|
||||||
|
The minimum CPU request for the agent containers.
|
||||||
|
|
||||||
|
Linux Pods: Defaults to `100m` (100 milliCPUs).
|
||||||
|
Windows Pods: Defaults to `100m` (100 milliCPUs).
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-limits-memory">
|
||||||
|
The maximum memory limit for the agent containers.
|
||||||
|
|
||||||
|
Linux Pods: Defaults to `128Mi` (128 megabytes).
|
||||||
|
Windows Pods: Defaults to `512Mi` (512 megabytes).
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-requests-memory">
|
||||||
|
The minimum memory request for the agent containers.
|
||||||
|
|
||||||
|
Linux Pods: Defaults to `64Mi` (64 megabytes).
|
||||||
|
Windows Pods: Defaults to `256Mi` (256 megabytes).
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-limits-ephemeral">
|
||||||
|
The maximum ephemeral storage limit for the agent containers. Doesn't have an explicit default value. The default value will conform to the default ephemeral storage limit for the pod.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="org.infisical.com/agent-requests-ephemeral">
|
||||||
|
The minimum ephemeral storage request for the agent containers. Doesn't have an explicit default value. The default value will conform to the default ephemeral storage request for the pod.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
</AccordionGroup>
|
||||||
|
|
||||||
## ConfigMap Configuration
|
## ConfigMap Configuration
|
||||||
|
|
||||||
### Supported Fields
|
### Supported Fields
|
||||||
@@ -141,18 +205,22 @@ The Infisical Agent Injector supports the following annotations:
|
|||||||
When you are configuring a pod to use the injector, you must create a config map in the same namespace as the pod you want to inject secrets into.
|
When you are configuring a pod to use the injector, you must create a config map in the same namespace as the pod you want to inject secrets into.
|
||||||
The entire config needs to be of string format and needs to be assigned to the `config.yaml` key in the config map. You can find a full example of the config at the end of this section.
|
The entire config needs to be of string format and needs to be assigned to the `config.yaml` key in the config map. You can find a full example of the config at the end of this section.
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
<Accordion title="infisical.address">
|
<Accordion title="infisical.address">
|
||||||
The address of your Infisical instance. This field is optional and will default to `https://app.infisical.com` if not provided.
|
The address of your Infisical instance. This field is optional and will default to `https://app.infisical.com` if not provided.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="infisical.revoke-credentials-on-shutdown">
|
<Accordion title="infisical.revoke-credentials-on-shutdown">
|
||||||
Whether to revoke all managed dynamic secret leases and identity access tokens on shutdown. Default: `"false"`.
|
Whether to revoke all managed dynamic secret leases and machine identity access tokens on shutdown. Default: `"false"`.
|
||||||
|
|
||||||
|
If this is set to `true`, all managed dynamic secret leases and machine identity access tokens will be revoked when a `SIGTERM` signal is sent to the agents container _(such as when a pod is terminated or when the pod is restarted)_.
|
||||||
|
|
||||||
If this is set to `true`, all managed dynamic secret leases and identity access tokens will be revoked when a `SIGTERM` signal is sent to the agents container _(such as when a pod is terminated or when the pod is restarted)_.
|
|
||||||
**Note:** In disaster events such as cluster power outages, a `SIGTERM` signal won't be sent to the agents container, and the credentials will not be revoked.
|
**Note:** In disaster events such as cluster power outages, a `SIGTERM` signal won't be sent to the agents container, and the credentials will not be revoked.
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Note that this is currently unsupported on Windows-based pods, and will only work when injecting into Linux-based pods.
|
This is currently unsupported on Windows-based pods, and will only work when injecting into Linux-based pods.
|
||||||
|
|
||||||
|
It's recommended to use the annotation `org.infisical.com/agent-revoke-on-shutdown: "true"` instead of configuring the revoke on shutdown on the config map. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the revoke on shutdown through annotations.
|
||||||
</Note>
|
</Note>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
@@ -162,8 +230,59 @@ The entire config needs to be of string format and needs to be assigned to the `
|
|||||||
Please note that the pod's default service account will be used to authenticate with Infisical.
|
Please note that the pod's default service account will be used to authenticate with Infisical.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
|
||||||
<Accordion title="infisical.auth.config.identity-id">
|
<Accordion title="infisical.auth.config.identity-id">
|
||||||
The ID of the machine identity to use to connect to Infisical. This field is required if the `infisical.auth.type` is set to `kubernetes`.
|
The ID of the machine identity to use for Kubernetes or LDAP authentication. This field is required if the `infisical.auth.type` is set to `kubernetes`.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="infisical.auth.config.username">
|
||||||
|
The LDAP username to use for LDAP authentication.
|
||||||
|
This field is required if the `infisical.auth.type` is set to `ldap-auth`.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="infisical.auth.config.password">
|
||||||
|
The LDAP password to use for LDAP authentication.
|
||||||
|
This field is required if the `infisical.auth.type` is set to `ldap-auth`.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="infisical.retry-strategy.max-retries">
|
||||||
|
How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You can also configure the max retries through annotations. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the max retries through annotations.
|
||||||
|
</Note>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="infisical.retry-strategy.max-delay">
|
||||||
|
The maximum delay between retries. Defaults to `5s` (5 seconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You can also configure the max delay through annotations. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the max delay through annotations.
|
||||||
|
</Note>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="infisical.retry-strategy.base-delay">
|
||||||
|
The base delay between retries. Defaults to `200ms` (200 milliseconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You can also configure the base delay through annotations. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the base delay through annotations.
|
||||||
|
</Note>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="cache.persistent.type">
|
||||||
|
The type of persistent caching to use. Currently only `kubernetes` is available, and will only work within Kubernetes environments.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
It is recommended to use the annotation `org.infisical.com/agent-cache-enabled: "true"` instead of configuring the cache on the config map. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the cache through annotations.
|
||||||
|
</Note>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="cache.persistent.service-account-token-path">
|
||||||
|
The path to the Kubernetes service account token to use for encrypting the persistent cache. Required when using `kubernetes` cache type. Defaults to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
It is recommended to use the annotation `org.infisical.com/agent-cache-enabled: "true"` instead of configuring the cache on the config map. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the cache through annotations.
|
||||||
|
</Note>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="templates[]">
|
<Accordion title="templates[]">
|
||||||
@@ -180,6 +299,7 @@ The templates hold an array of templates that will be rendered and injected into
|
|||||||
This will be rendered as a [Go Template](https://pkg.go.dev/text/template) and will have access to the following variables.
|
This will be rendered as a [Go Template](https://pkg.go.dev/text/template) and will have access to the following variables.
|
||||||
It follows the templating format and supports the same functions as the [Infisical Agent](/integrations/platforms/infisical-agent#quick-start-infisical-agent)
|
It follows the templating format and supports the same functions as the [Infisical Agent](/integrations/platforms/infisical-agent#quick-start-infisical-agent)
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
@@ -271,7 +391,7 @@ The Infisical Agent Injector supports Machine Identity [Kubernetes Auth](/docume
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
|
|
||||||
To use the config map in your pod, you will need to add the `org.infisical.com/agent-config-map` annotation to your pod's deployment. The value of the annotation is the name of the config map you created above.
|
To use the config map in your pod, you will need to add the `org.infisical.com/agent-config-map` annotation to your pod's deployment. The value of the annotation is the name of the config map you created above. The config map must be in the same namespace as the pod you're injecting into.
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Pod
|
kind: Pod
|
||||||
|
|||||||
@@ -1,54 +1,367 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from "react";
|
||||||
|
|
||||||
export const AppConnectionsBrowser = () => {
|
export const AppConnectionsBrowser = () => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
const [selectedCategory, setSelectedCategory] = useState("All");
|
||||||
|
|
||||||
const categories = ['All', 'Cloud Providers', 'Databases', 'CI/CD', 'Monitoring', 'Directory Services', 'Identity & Auth', 'Data Analytics', 'Hosting', 'DevOps Tools', 'Security'];
|
const categories = [
|
||||||
|
"All",
|
||||||
|
"Cloud Providers",
|
||||||
|
"Databases",
|
||||||
|
"CI/CD",
|
||||||
|
"Monitoring",
|
||||||
|
"Directory Services",
|
||||||
|
"Identity & Auth",
|
||||||
|
"Data Analytics",
|
||||||
|
"Hosting",
|
||||||
|
"DevOps Tools",
|
||||||
|
"Security",
|
||||||
|
"Networking & DNS",
|
||||||
|
];
|
||||||
|
|
||||||
const connections = [
|
const connections = [
|
||||||
{"name": "AWS", "slug": "aws", "path": "/integrations/app-connections/aws", "description": "Learn how to connect your AWS applications to pull secrets from Infisical.", "category": "Cloud Providers"},
|
{
|
||||||
{"name": "Azure Key Vault", "slug": "azure-key-vault", "path": "/integrations/app-connections/azure-key-vault", "description": "Learn how to connect your Azure Key Vault to pull secrets from Infisical.", "category": "Cloud Providers"},
|
name: "AWS",
|
||||||
{"name": "Azure App Configuration", "slug": "azure-app-configuration", "path": "/integrations/app-connections/azure-app-configuration", "description": "Learn how to connect your Azure App Configuration to pull secrets from Infisical.", "category": "Cloud Providers"},
|
slug: "aws",
|
||||||
{"name": "Azure Client Secrets", "slug": "azure-client-secrets", "path": "/integrations/app-connections/azure-client-secrets", "description": "Learn how to connect your Azure Client Secrets to pull secrets from Infisical.", "category": "Cloud Providers"},
|
path: "/integrations/app-connections/aws",
|
||||||
{"name": "Azure DevOps", "slug": "azure-devops", "path": "/integrations/app-connections/azure-devops", "description": "Learn how to connect your Azure DevOps to pull secrets from Infisical.", "category": "CI/CD"},
|
description:
|
||||||
{"name": "Azure ADCS", "slug": "azure-adcs", "path": "/integrations/app-connections/azure-adcs", "description": "Learn how to connect your Azure ADCS to pull secrets from Infisical.", "category": "Cloud Providers"},
|
"Learn how to connect your AWS applications to pull secrets from Infisical.",
|
||||||
{"name": "GCP", "slug": "gcp", "path": "/integrations/app-connections/gcp", "description": "Learn how to connect your GCP applications to pull secrets from Infisical.", "category": "Cloud Providers"},
|
category: "Cloud Providers",
|
||||||
{"name": "HashiCorp Vault", "slug": "hashicorp-vault", "path": "/integrations/app-connections/hashicorp-vault", "description": "Learn how to connect your HashiCorp Vault to pull secrets from Infisical.", "category": "Security"},
|
},
|
||||||
{"name": "1Password", "slug": "1password", "path": "/integrations/app-connections/1password", "description": "Learn how to connect your 1Password to pull secrets from Infisical.", "category": "Security"},
|
{
|
||||||
{"name": "Vercel", "slug": "vercel", "path": "/integrations/app-connections/vercel", "description": "Learn how to connect your Vercel application to pull secrets from Infisical.", "category": "Hosting"},
|
name: "Azure Key Vault",
|
||||||
{"name": "Netlify", "slug": "netlify", "path": "/integrations/app-connections/netlify", "description": "Learn how to connect your Netlify application to pull secrets from Infisical.", "category": "Hosting"},
|
slug: "azure-key-vault",
|
||||||
{"name": "Railway", "slug": "railway", "path": "/integrations/app-connections/railway", "description": "Learn how to connect your Railway application to pull secrets from Infisical.", "category": "Hosting"},
|
path: "/integrations/app-connections/azure-key-vault",
|
||||||
{"name": "Fly.io", "slug": "flyio", "path": "/integrations/app-connections/flyio", "description": "Learn how to connect your Fly.io application to pull secrets from Infisical.", "category": "Hosting"},
|
description:
|
||||||
{"name": "Render", "slug": "render", "path": "/integrations/app-connections/render", "description": "Learn how to connect your Render application to pull secrets from Infisical.", "category": "Hosting"},
|
"Learn how to connect your Azure Key Vault to pull secrets from Infisical.",
|
||||||
{"name": "Heroku", "slug": "heroku", "path": "/integrations/app-connections/heroku", "description": "Learn how to connect your Heroku application to pull secrets from Infisical.", "category": "Hosting"},
|
category: "Cloud Providers",
|
||||||
{"name": "DigitalOcean", "slug": "digital-ocean", "path": "/integrations/app-connections/digital-ocean", "description": "Learn how to connect your DigitalOcean application to pull secrets from Infisical.", "category": "Hosting"},
|
},
|
||||||
{"name": "Supabase", "slug": "supabase", "path": "/integrations/app-connections/supabase", "description": "Learn how to connect your Supabase application to pull secrets from Infisical.", "category": "Databases"},
|
{
|
||||||
{"name": "Checkly", "slug": "checkly", "path": "/integrations/app-connections/checkly", "description": "Learn how to connect your Checkly application to pull secrets from Infisical.", "category": "Monitoring"},
|
name: "Azure App Configuration",
|
||||||
{"name": "GitHub", "slug": "github", "path": "/integrations/app-connections/github", "description": "Learn how to connect your GitHub application to pull secrets from Infisical.", "category": "CI/CD"},
|
slug: "azure-app-configuration",
|
||||||
{"name": "GitHub Radar", "slug": "github-radar", "path": "/integrations/app-connections/github-radar", "description": "Learn how to connect your GitHub Radar to pull secrets from Infisical.", "category": "CI/CD"},
|
path: "/integrations/app-connections/azure-app-configuration",
|
||||||
{"name": "GitLab", "slug": "gitlab", "path": "/integrations/app-connections/gitlab", "description": "Learn how to connect your GitLab application to pull secrets from Infisical.", "category": "CI/CD"},
|
description:
|
||||||
{"name": "TeamCity", "slug": "teamcity", "path": "/integrations/app-connections/teamcity", "description": "Learn how to connect your TeamCity to pull secrets from Infisical.", "category": "CI/CD"},
|
"Learn how to connect your Azure App Configuration to pull secrets from Infisical.",
|
||||||
{"name": "Bitbucket", "slug": "bitbucket", "path": "/integrations/app-connections/bitbucket", "description": "Learn how to connect your Bitbucket to pull secrets from Infisical.", "category": "CI/CD"},
|
category: "Cloud Providers",
|
||||||
{"name": "Terraform Cloud", "slug": "terraform-cloud", "path": "/integrations/app-connections/terraform-cloud", "description": "Learn how to connect your Terraform Cloud to pull secrets from Infisical.", "category": "DevOps Tools"},
|
},
|
||||||
{"name": "Cloudflare", "slug": "cloudflare", "path": "/integrations/app-connections/cloudflare", "description": "Learn how to connect your Cloudflare application to pull secrets from Infisical.", "category": "Cloud Providers"},
|
{
|
||||||
{"name": "Databricks", "slug": "databricks", "path": "/integrations/app-connections/databricks", "description": "Learn how to connect your Databricks to pull secrets from Infisical.", "category": "Data Analytics"},
|
name: "Azure Client Secrets",
|
||||||
{"name": "Windmill", "slug": "windmill", "path": "/integrations/app-connections/windmill", "description": "Learn how to connect your Windmill to pull secrets from Infisical.", "category": "DevOps Tools"},
|
slug: "azure-client-secrets",
|
||||||
{"name": "Camunda", "slug": "camunda", "path": "/integrations/app-connections/camunda", "description": "Learn how to connect your Camunda to pull secrets from Infisical.", "category": "DevOps Tools"},
|
path: "/integrations/app-connections/azure-client-secrets",
|
||||||
{"name": "Humanitec", "slug": "humanitec", "path": "/integrations/app-connections/humanitec", "description": "Learn how to connect your Humanitec to pull secrets from Infisical.", "category": "DevOps Tools"},
|
description:
|
||||||
{"name": "OCI", "slug": "oci", "path": "/integrations/app-connections/oci", "description": "Learn how to connect your OCI applications to pull secrets from Infisical.", "category": "Cloud Providers"},
|
"Learn how to connect your Azure Client Secrets to pull secrets from Infisical.",
|
||||||
{"name": "Zabbix", "slug": "zabbix", "path": "/integrations/app-connections/zabbix", "description": "Learn how to connect your Zabbix to pull secrets from Infisical.", "category": "Monitoring"},
|
category: "Cloud Providers",
|
||||||
{"name": "MySQL", "slug": "mysql", "path": "/integrations/app-connections/mysql", "description": "Learn how to connect your MySQL database to pull secrets from Infisical.", "category": "Databases"},
|
},
|
||||||
{"name": "PostgreSQL", "slug": "postgres", "path": "/integrations/app-connections/postgres", "description": "Learn how to connect your PostgreSQL database to pull secrets from Infisical.", "category": "Databases"},
|
{
|
||||||
{"name": "Microsoft SQL Server", "slug": "mssql", "path": "/integrations/app-connections/mssql", "description": "Learn how to connect your SQL Server database to pull secrets from Infisical.", "category": "Databases"},
|
name: "Azure DevOps",
|
||||||
{"name": "Oracle Database", "slug": "oracledb", "path": "/integrations/app-connections/oracledb", "description": "Learn how to connect your Oracle database to pull secrets from Infisical.", "category": "Databases"},
|
slug: "azure-devops",
|
||||||
{"name": "Redis", "slug": "redis", "path": "/integrations/app-connections/redis", "description": "Learn how to connect Redis to pull secrets from Infisical.", "category": "Databases"},
|
path: "/integrations/app-connections/azure-devops",
|
||||||
{"name": "LDAP", "slug": "ldap", "path": "/integrations/app-connections/ldap", "description": "Learn how to connect your LDAP to pull secrets from Infisical.", "category": "Directory Services"},
|
description:
|
||||||
{"name": "Auth0", "slug": "auth0", "path": "/integrations/app-connections/auth0", "description": "Learn how to connect your Auth0 to pull secrets from Infisical.", "category": "Identity & Auth"},
|
"Learn how to connect your Azure DevOps to pull secrets from Infisical.",
|
||||||
{"name": "Okta", "slug": "okta", "path": "/integrations/app-connections/okta", "description": "Learn how to connect your Okta to pull secrets from Infisical.", "category": "Identity & Auth"},
|
category: "CI/CD",
|
||||||
{"name": "Laravel Forge", "slug": "laravel-forge", "path": "/integrations/app-connections/laravel-forge", "description": "Learn how to connect your Laravel Forge to pull secrets from Infisical.", "category": "Hosting"},
|
},
|
||||||
{"name": "Chef", "slug": "chef", "path": "/integrations/app-connections/chef", "description": "Learn how to connect your Chef to pull secrets from Infisical.", "category": "DevOps Tools"},
|
{
|
||||||
{"name": "Northflank", "slug": "northflank", "path": "/integrations/app-connections/northflank", "description": "Learn how to connect your Northflank projects to pull secrets from Infisical.", "category": "Hosting"}
|
name: "Azure ADCS",
|
||||||
|
slug: "azure-adcs",
|
||||||
|
path: "/integrations/app-connections/azure-adcs",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Azure ADCS to pull secrets from Infisical.",
|
||||||
|
category: "Cloud Providers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GCP",
|
||||||
|
slug: "gcp",
|
||||||
|
path: "/integrations/app-connections/gcp",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your GCP applications to pull secrets from Infisical.",
|
||||||
|
category: "Cloud Providers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HashiCorp Vault",
|
||||||
|
slug: "hashicorp-vault",
|
||||||
|
path: "/integrations/app-connections/hashicorp-vault",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your HashiCorp Vault to pull secrets from Infisical.",
|
||||||
|
category: "Security",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1Password",
|
||||||
|
slug: "1password",
|
||||||
|
path: "/integrations/app-connections/1password",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your 1Password to pull secrets from Infisical.",
|
||||||
|
category: "Security",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Vercel",
|
||||||
|
slug: "vercel",
|
||||||
|
path: "/integrations/app-connections/vercel",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Vercel application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Netlify",
|
||||||
|
slug: "netlify",
|
||||||
|
path: "/integrations/app-connections/netlify",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Netlify application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Railway",
|
||||||
|
slug: "railway",
|
||||||
|
path: "/integrations/app-connections/railway",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Railway application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Fly.io",
|
||||||
|
slug: "flyio",
|
||||||
|
path: "/integrations/app-connections/flyio",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Fly.io application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Render",
|
||||||
|
slug: "render",
|
||||||
|
path: "/integrations/app-connections/render",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Render application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Heroku",
|
||||||
|
slug: "heroku",
|
||||||
|
path: "/integrations/app-connections/heroku",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Heroku application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DigitalOcean",
|
||||||
|
slug: "digital-ocean",
|
||||||
|
path: "/integrations/app-connections/digital-ocean",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your DigitalOcean application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Supabase",
|
||||||
|
slug: "supabase",
|
||||||
|
path: "/integrations/app-connections/supabase",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Supabase application to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Checkly",
|
||||||
|
slug: "checkly",
|
||||||
|
path: "/integrations/app-connections/checkly",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Checkly application to pull secrets from Infisical.",
|
||||||
|
category: "Monitoring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitHub",
|
||||||
|
slug: "github",
|
||||||
|
path: "/integrations/app-connections/github",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your GitHub application to pull secrets from Infisical.",
|
||||||
|
category: "CI/CD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitHub Radar",
|
||||||
|
slug: "github-radar",
|
||||||
|
path: "/integrations/app-connections/github-radar",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your GitHub Radar to pull secrets from Infisical.",
|
||||||
|
category: "CI/CD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitLab",
|
||||||
|
slug: "gitlab",
|
||||||
|
path: "/integrations/app-connections/gitlab",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your GitLab application to pull secrets from Infisical.",
|
||||||
|
category: "CI/CD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TeamCity",
|
||||||
|
slug: "teamcity",
|
||||||
|
path: "/integrations/app-connections/teamcity",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your TeamCity to pull secrets from Infisical.",
|
||||||
|
category: "CI/CD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bitbucket",
|
||||||
|
slug: "bitbucket",
|
||||||
|
path: "/integrations/app-connections/bitbucket",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Bitbucket to pull secrets from Infisical.",
|
||||||
|
category: "CI/CD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Terraform Cloud",
|
||||||
|
slug: "terraform-cloud",
|
||||||
|
path: "/integrations/app-connections/terraform-cloud",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Terraform Cloud to pull secrets from Infisical.",
|
||||||
|
category: "DevOps Tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cloudflare",
|
||||||
|
slug: "cloudflare",
|
||||||
|
path: "/integrations/app-connections/cloudflare",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Cloudflare application to pull secrets from Infisical.",
|
||||||
|
category: "Cloud Providers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Databricks",
|
||||||
|
slug: "databricks",
|
||||||
|
path: "/integrations/app-connections/databricks",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Databricks to pull secrets from Infisical.",
|
||||||
|
category: "Data Analytics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DNS Made Easy",
|
||||||
|
slug: "dns-made-easy",
|
||||||
|
path: "/integrations/app-connections/dns-made-easy",
|
||||||
|
description: "Learn how to connect Infisical to DNS Made Easy.",
|
||||||
|
category: "Networking & DNS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Windmill",
|
||||||
|
slug: "windmill",
|
||||||
|
path: "/integrations/app-connections/windmill",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Windmill to pull secrets from Infisical.",
|
||||||
|
category: "DevOps Tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Camunda",
|
||||||
|
slug: "camunda",
|
||||||
|
path: "/integrations/app-connections/camunda",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Camunda to pull secrets from Infisical.",
|
||||||
|
category: "DevOps Tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Humanitec",
|
||||||
|
slug: "humanitec",
|
||||||
|
path: "/integrations/app-connections/humanitec",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Humanitec to pull secrets from Infisical.",
|
||||||
|
category: "DevOps Tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OCI",
|
||||||
|
slug: "oci",
|
||||||
|
path: "/integrations/app-connections/oci",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your OCI applications to pull secrets from Infisical.",
|
||||||
|
category: "Cloud Providers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Zabbix",
|
||||||
|
slug: "zabbix",
|
||||||
|
path: "/integrations/app-connections/zabbix",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Zabbix to pull secrets from Infisical.",
|
||||||
|
category: "Monitoring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MySQL",
|
||||||
|
slug: "mysql",
|
||||||
|
path: "/integrations/app-connections/mysql",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your MySQL database to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PostgreSQL",
|
||||||
|
slug: "postgres",
|
||||||
|
path: "/integrations/app-connections/postgres",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your PostgreSQL database to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Microsoft SQL Server",
|
||||||
|
slug: "mssql",
|
||||||
|
path: "/integrations/app-connections/mssql",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your SQL Server database to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Oracle Database",
|
||||||
|
slug: "oracledb",
|
||||||
|
path: "/integrations/app-connections/oracledb",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Oracle database to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Redis",
|
||||||
|
slug: "redis",
|
||||||
|
path: "/integrations/app-connections/redis",
|
||||||
|
description: "Learn how to connect Redis to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LDAP",
|
||||||
|
slug: "ldap",
|
||||||
|
path: "/integrations/app-connections/ldap",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your LDAP to pull secrets from Infisical.",
|
||||||
|
category: "Directory Services",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Auth0",
|
||||||
|
slug: "auth0",
|
||||||
|
path: "/integrations/app-connections/auth0",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Auth0 to pull secrets from Infisical.",
|
||||||
|
category: "Identity & Auth",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Okta",
|
||||||
|
slug: "okta",
|
||||||
|
path: "/integrations/app-connections/okta",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Okta to pull secrets from Infisical.",
|
||||||
|
category: "Identity & Auth",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Laravel Forge",
|
||||||
|
slug: "laravel-forge",
|
||||||
|
path: "/integrations/app-connections/laravel-forge",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Laravel Forge to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Chef",
|
||||||
|
slug: "chef",
|
||||||
|
path: "/integrations/app-connections/chef",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Chef to pull secrets from Infisical.",
|
||||||
|
category: "DevOps Tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Northflank",
|
||||||
|
slug: "northflank",
|
||||||
|
path: "/integrations/app-connections/northflank",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Northflank projects to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
].sort(function (a, b) {
|
].sort(function (a, b) {
|
||||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||||
});
|
});
|
||||||
@@ -56,14 +369,19 @@ export const AppConnectionsBrowser = () => {
|
|||||||
const filteredConnections = useMemo(() => {
|
const filteredConnections = useMemo(() => {
|
||||||
let filtered = connections;
|
let filtered = connections;
|
||||||
|
|
||||||
if (selectedCategory !== 'All') {
|
if (selectedCategory !== "All") {
|
||||||
filtered = filtered.filter(connection => connection.category === selectedCategory);
|
filtered = filtered.filter(
|
||||||
|
(connection) => connection.category === selectedCategory
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
filtered = filtered.filter(connection =>
|
filtered = filtered.filter(
|
||||||
|
(connection) =>
|
||||||
connection.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
connection.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
connection.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
connection.description
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase()) ||
|
||||||
connection.category.toLowerCase().includes(searchTerm.toLowerCase())
|
connection.category.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -77,8 +395,18 @@ export const AppConnectionsBrowser = () => {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<svg className="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
className="h-4 w-4 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -94,14 +422,14 @@ export const AppConnectionsBrowser = () => {
|
|||||||
{/* Category Filter */}
|
{/* Category Filter */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{categories.map(category => (
|
{categories.map((category) => (
|
||||||
<button
|
<button
|
||||||
key={category}
|
key={category}
|
||||||
onClick={() => setSelectedCategory(category)}
|
onClick={() => setSelectedCategory(category)}
|
||||||
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors shadow-sm ${
|
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors shadow-sm ${
|
||||||
selectedCategory === category
|
selectedCategory === category
|
||||||
? 'bg-yellow-100 text-yellow-700 border border-yellow-200'
|
? "bg-yellow-100 text-yellow-700 border border-yellow-200"
|
||||||
: 'bg-white text-gray-700 border border-gray-200 hover:bg-yellow-50 hover:border-yellow-200'
|
: "bg-white text-gray-700 border border-gray-200 hover:bg-yellow-50 hover:border-yellow-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
@@ -113,8 +441,9 @@ export const AppConnectionsBrowser = () => {
|
|||||||
{/* Results Count */}
|
{/* Results Count */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{filteredConnections.length} app connection{filteredConnections.length !== 1 ? 's' : ''} found
|
{filteredConnections.length} app connection
|
||||||
{selectedCategory !== 'All' && ` in ${selectedCategory}`}
|
{filteredConnections.length !== 1 ? "s" : ""} found
|
||||||
|
{selectedCategory !== "All" && ` in ${selectedCategory}`}
|
||||||
{searchTerm && ` for "${searchTerm}"`}
|
{searchTerm && ` for "${searchTerm}"`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,9 +476,13 @@ export const AppConnectionsBrowser = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="flex flex-col items-center space-y-2">
|
<div className="flex flex-col items-center space-y-2">
|
||||||
<p className="text-gray-500">No app connections found matching your criteria</p>
|
<p className="text-gray-500">
|
||||||
|
No app connections found matching your criteria
|
||||||
|
</p>
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<p className="text-gray-400 text-sm">Try adjusting your search terms or filters</p>
|
<p className="text-gray-400 text-sm">
|
||||||
|
Try adjusting your search terms or filters
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 120 60" width="120" height="60" style="enable-background:new 0 0 120 60;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#808285;}
|
||||||
|
.st1{fill:url(#SVGID_1_);}
|
||||||
|
.st2{fill:#005B99;}
|
||||||
|
.st3{enable-background:new ;}
|
||||||
|
.st4{fill:#77787B;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M34.1,42.6h9.4v-3.2C41.5,41.2,38.2,42.3,34.1,42.6L34.1,42.6z"/>
|
||||||
|
<path class="st0" d="M17.3,40.1v2.5h11.9c-2.4-0.2-5-0.6-7.6-1.2C20.1,41,18.7,40.6,17.3,40.1L17.3,40.1z"/>
|
||||||
|
|
||||||
|
<radialGradient id="SVGID_1_" cx="-183.5882" cy="811.1324" r="47.498" gradientTransform="matrix(1 0 0 1 241.0864 -776.3726)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" style="stop-color:#0095DA"/>
|
||||||
|
<stop offset="0.21" style="stop-color:#0095DA"/>
|
||||||
|
<stop offset="0.33" style="stop-color:#00ACE4"/>
|
||||||
|
<stop offset="0.9045" style="stop-color:#005093"/>
|
||||||
|
<stop offset="0.9335" style="stop-color:#005396"/>
|
||||||
|
<stop offset="0.954" style="stop-color:#005C9F"/>
|
||||||
|
<stop offset="0.9718" style="stop-color:#006BAE"/>
|
||||||
|
<stop offset="0.988" style="stop-color:#0080C4"/>
|
||||||
|
<stop offset="1" style="stop-color:#0095DA"/>
|
||||||
|
</radialGradient>
|
||||||
|
<path class="st1" d="M43.5,18.5H29.2C23,17.1,15.4,17.1,10,18.2c2.4-0.3,5.1-0.3,7.8,0.1c0.7,0.1,1.3,0.2,2,0.3l0,0
|
||||||
|
c2.9,0.5,5.7,1.2,8.1,2.2l0,0c0.3,0.1,0.5,0.2,0.8,0.3h0c0.1,0,0.2,0.1,0.3,0.1h0c0.1,0,0.2,0.1,0.3,0.1h0c0.1,0,0.2,0.1,0.2,0.1
|
||||||
|
l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.2,0.1,0.2,0.1l0,0l0,0h0c0.1,0,0.1,0.1,0.2,0.1l0,0
|
||||||
|
c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1
|
||||||
|
l0.1,0c0.1,0,0.1,0.1,0.2,0.1l0.1,0c0.1,0,0.1,0.1,0.2,0.1l0.1,0c0.1,0,0.1,0.1,0.2,0.1l0.1,0c0.1,0,0.1,0.1,0.1,0.1l0.1,0.1
|
||||||
|
c0.1,0,0.1,0.1,0.1,0.1l0.1,0.1c0.1,0,0.1,0.1,0.1,0.1l0.1,0.1c0,0,0.1,0.1,0.1,0.1c3.2,2.2,5.1,4.9,5.1,7.6c0,3.1-2.6,5.7-6.8,7.2
|
||||||
|
c2.9-1.5,4.6-3.6,4.6-6.1c0-3.1-2.7-6.3-7-8.7c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1
|
||||||
|
c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1l-0.2-0.1c-0.1,0-0.2-0.1-0.2-0.1
|
||||||
|
l-0.1-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1
|
||||||
|
L26,22c-0.1,0-0.2-0.1-0.3-0.1h0l-0.2-0.1l0,0c-0.1,0-0.2-0.1-0.3-0.1l-0.1,0c-0.1-0.1-0.2-0.1-0.4-0.1h0c-0.1,0-0.2-0.1-0.3-0.1h0
|
||||||
|
c-0.1-0.1-0.3-0.1-0.5-0.2l-0.1,0c-0.1-0.1-0.3-0.1-0.5-0.1h0c-0.3-0.1-0.5-0.2-0.8-0.3l0,0c-0.3-0.1-0.6-0.2-0.8-0.3l0,0
|
||||||
|
c-0.2-0.1-0.3-0.1-0.5-0.1l0,0c-0.7-0.2-1.5-0.4-2.3-0.5l0,0c-0.6-0.1-1.2-0.2-1.8-0.3v19.2c0.2,0.1,0.4,0.1,0.6,0.1
|
||||||
|
C30.3,42,41,39.4,41.8,33.2c0.4-3.3-2-6.9-6.1-10c3.3,1.9,6,4.1,7.8,6.4c1,1.4,1.7,2.7,2,4.1c-0.1-1.9-0.9-4.8-2-6.9L43.5,18.5
|
||||||
|
L43.5,18.5z"/>
|
||||||
|
<g>
|
||||||
|
<path class="st2" d="M67.5,26.9c0,1-0.2,2-0.7,2.9c-0.4,0.9-1,1.7-1.8,2.3c-0.8,0.7-1.7,1.2-2.8,1.6c-1.1,0.4-2.2,0.6-3.5,0.6H49
|
||||||
|
v-9h4.5v5.3h5.2c0.6,0,1.2-0.1,1.7-0.3c0.5-0.2,1-0.4,1.4-0.7c0.4-0.3,0.7-0.7,0.9-1.1c0.2-0.4,0.3-0.9,0.3-1.4
|
||||||
|
c0-0.5-0.1-1-0.3-1.4c-0.2-0.4-0.5-0.8-0.9-1.1c-0.4-0.3-0.8-0.6-1.4-0.7c-0.5-0.2-1.1-0.3-1.7-0.3H49l2.9-3.8h6.8
|
||||||
|
c1.3,0,2.4,0.2,3.5,0.5c1.1,0.3,2,0.8,2.8,1.5s1.4,1.4,1.8,2.3C67.2,24.9,67.5,25.8,67.5,26.9z"/>
|
||||||
|
<g>
|
||||||
|
<path class="st2" d="M82.8,21.4v6.8l-8.9-8c-0.4-0.3-0.7-0.5-1-0.6c-0.3-0.1-0.6-0.1-0.8-0.1c-0.3,0-0.6,0.1-0.9,0.1
|
||||||
|
c-0.3,0.1-0.6,0.3-0.8,0.5c-0.2,0.2-0.4,0.5-0.5,0.8c-0.1,0.3-0.2,0.8-0.2,1.2v12h4.1v-8.5l8.9,8c0.3,0.3,0.7,0.5,0.9,0.6
|
||||||
|
c0.3,0.1,0.6,0.1,0.8,0.1c0.3,0,0.6-0.1,0.9-0.1c0.3-0.1,0.6-0.3,0.8-0.5c0.2-0.2,0.4-0.5,0.5-0.8c0.1-0.3,0.2-0.8,0.2-1.2V19.9
|
||||||
|
L82.8,21.4z"/>
|
||||||
|
</g>
|
||||||
|
<path class="st2" d="M103,25.5c1.8,0,3.1,0.3,4,1c0.9,0.7,1.4,1.6,1.4,3c0,0.7-0.1,1.4-0.3,2c-0.2,0.6-0.6,1.1-1.1,1.5
|
||||||
|
c-0.5,0.4-1.2,0.7-1.9,0.9c-0.8,0.2-1.7,0.3-2.8,0.3H88.7l2.9-3.7h11c0.5,0,0.9-0.1,1.2-0.3c0.3-0.2,0.4-0.4,0.4-0.8
|
||||||
|
s-0.1-0.7-0.4-0.8c-0.3-0.2-0.6-0.2-1.2-0.2h-7.9c-0.9,0-1.8-0.1-2.4-0.3c-0.7-0.2-1.2-0.5-1.7-0.8c-0.5-0.4-0.8-0.8-1-1.3
|
||||||
|
c-0.2-0.5-0.3-1.1-0.3-1.7c0-0.7,0.1-1.3,0.4-1.9c0.2-0.6,0.6-1,1.1-1.4c0.5-0.4,1.1-0.7,1.9-0.9c0.8-0.2,1.7-0.3,2.8-0.3h12.6
|
||||||
|
l-2.9,3.8H95.1c-0.5,0-0.9,0.1-1.2,0.2c-0.3,0.1-0.4,0.4-0.4,0.8c0,0.4,0.1,0.6,0.4,0.8c0.3,0.1,0.6,0.2,1.2,0.2L103,25.5
|
||||||
|
L103,25.5z"/>
|
||||||
|
</g>
|
||||||
|
<g class="st3">
|
||||||
|
<path class="st4" d="M57.5,36.6v5h-1.4v-2.7v-0.7l0.1-0.4v-0.4h-0.1l-0.2,0.3l-0.2,0.3l-0.4,0.7l-1.7,2.8h-1.3l-1.7-2.8l-0.4-0.7
|
||||||
|
l-0.2-0.3l-0.2-0.3h-0.1l0,0.4v0.4l0.1,0.7v2.7h-1.5v-5h2.4l1.4,2.3l0.4,0.7l0.2,0.3l0.2,0.3H53l0.2-0.3l0.2-0.3l0.4-0.7l1.4-2.3
|
||||||
|
L57.5,36.6L57.5,36.6z"/>
|
||||||
|
<path class="st4" d="M63.6,40.6h-3.3l-0.5,0.9h-1.6l2.6-5H63l2.6,5H64L63.6,40.6z M63.2,39.9l-1.3-2.6l-1.3,2.6H63.2z"/>
|
||||||
|
<path class="st4" d="M66.2,41.6v-5H70c1,0,1.8,0.2,2.2,0.5s0.7,0.8,0.7,1.6c0,0.7,0,1.2-0.1,1.5c0,0.3-0.2,0.6-0.5,0.9
|
||||||
|
c-0.3,0.3-1,0.5-2.3,0.5H66.2L66.2,41.6z M67.7,40.7h2.1c0.7,0,1.2-0.1,1.4-0.3s0.3-0.7,0.3-1.4c0-0.7-0.1-1.2-0.3-1.4
|
||||||
|
s-0.6-0.3-1.3-0.3h-2.2L67.7,40.7L67.7,40.7z"/>
|
||||||
|
<path class="st4" d="M75.3,37.4v1.3h3.6v0.7h-3.6v1.4h3.8v0.8h-5.3v-5h5.3v0.8L75.3,37.4L75.3,37.4z"/>
|
||||||
|
<path class="st4" d="M84.4,37.4v1.3H88v0.7h-3.6v1.4h3.8v0.8H83v-5h5.3v0.8L84.4,37.4L84.4,37.4z"/>
|
||||||
|
<path class="st4" d="M94,40.6h-3.3l-0.5,0.9h-1.6l2.6-5h2.2l2.6,5h-1.5L94,40.6z M93.7,39.9l-1.3-2.6L91,39.9H93.7z"/>
|
||||||
|
<path class="st4" d="M102.4,38.1H101v-0.1c0-0.3-0.1-0.4-0.3-0.5c-0.2-0.1-0.6-0.1-1.2-0.1c-0.7,0-1.2,0-1.4,0.1
|
||||||
|
c-0.2,0.1-0.3,0.3-0.3,0.5c0,0.3,0.1,0.5,0.3,0.6s0.8,0.1,1.7,0.1c1.1,0,1.9,0.1,2.2,0.3c0.3,0.2,0.5,0.5,0.5,1.1
|
||||||
|
c0,0.7-0.2,1.1-0.6,1.3s-1.2,0.3-2.5,0.3c-1.3,0-2.2-0.1-2.5-0.3c-0.4-0.2-0.6-0.6-0.6-1.2V40h1.4v0.1c0,0.3,0.1,0.5,0.3,0.6
|
||||||
|
c0.2,0.1,0.7,0.1,1.5,0.1c0.7,0,1.1,0,1.2-0.1s0.3-0.3,0.3-0.6c0-0.3-0.1-0.4-0.2-0.5c-0.1-0.1-0.4-0.1-0.9-0.1l-0.8,0
|
||||||
|
c-1.2,0-2-0.1-2.3-0.3c-0.4-0.2-0.5-0.6-0.5-1.1c0-0.6,0.2-1,0.6-1.2c0.4-0.2,1.2-0.3,2.5-0.3c1.1,0,1.9,0.1,2.3,0.3
|
||||||
|
c0.4,0.2,0.6,0.5,0.6,1L102.4,38.1L102.4,38.1z"/>
|
||||||
|
<path class="st4" d="M110,36.6l-2.9,3.1v1.9h-1.5v-1.9l-2.8-3.1h1.7l1.2,1.3l0.3,0.4l0.2,0.2l0.2,0.2l0.2-0.2l0.2-0.2l0.3-0.4
|
||||||
|
l1.2-1.3H110z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.2 KiB |
@@ -289,7 +289,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"title": "Settings",
|
"title": "Project Settings",
|
||||||
"description": "These settings only apply to the currently selected Project.",
|
"description": "These settings only apply to the currently selected Project.",
|
||||||
"danger-zone": "Danger Zone",
|
"danger-zone": "Danger Zone",
|
||||||
"delete-project": "Delete Project",
|
"delete-project": "Delete Project",
|
||||||
|
|||||||
@@ -87,17 +87,24 @@ const shouldShowConditionalAccess = (
|
|||||||
folderPath: string,
|
folderPath: string,
|
||||||
conditionalFields: string[]
|
conditionalFields: string[]
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return actionRuleMap.some((rule) => {
|
// Find all rules that apply to this environment/path
|
||||||
|
const applicableRules = actionRuleMap.filter((rule) => {
|
||||||
const ruleConditions = rule[action]?.conditions;
|
const ruleConditions = rule[action]?.conditions;
|
||||||
if (!ruleConditions) return false;
|
if (!ruleConditions) return false;
|
||||||
|
|
||||||
// Check if any of the conditional fields are present
|
|
||||||
const hasConditionalField = conditionalFields.some((field) => ruleConditions[field]);
|
|
||||||
if (!hasConditionalField) return false;
|
|
||||||
|
|
||||||
// Check if base conditions (environment and secretPath) apply
|
|
||||||
return doBaseConditionsApply(ruleConditions, environment, folderPath);
|
return doBaseConditionsApply(ruleConditions, environment, folderPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If no rules apply, don't show conditional
|
||||||
|
if (applicableRules.length === 0) return false;
|
||||||
|
|
||||||
|
// Check if ALL applicable rules have conditional fields and if at least one rule applies without conditional fields, show full access
|
||||||
|
const allRulesHaveConditionalFields = applicableRules.every((rule) => {
|
||||||
|
const ruleConditions = rule[action]?.conditions;
|
||||||
|
if (!ruleConditions) return false;
|
||||||
|
return conditionalFields.some((field) => ruleConditions[field]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return allRulesHaveConditionalFields;
|
||||||
};
|
};
|
||||||
|
|
||||||
const determineAccessLevel = (
|
const determineAccessLevel = (
|
||||||
|
|||||||
@@ -3,14 +3,7 @@ import { ReactNode } from "@tanstack/react-router";
|
|||||||
import { LucideIcon } from "lucide-react";
|
import { LucideIcon } from "lucide-react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import {
|
import { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon } from "@app/components/v3";
|
||||||
Badge,
|
|
||||||
InstanceIcon,
|
|
||||||
OrgIcon,
|
|
||||||
ProjectIcon,
|
|
||||||
SubOrgIcon,
|
|
||||||
TBadgeProps
|
|
||||||
} from "@app/components/v3";
|
|
||||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -21,41 +14,40 @@ type Props = {
|
|||||||
scope: "org" | "namespace" | "instance" | ProjectType | null;
|
scope: "org" | "namespace" | "instance" | ProjectType | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: LucideIcon }> = {
|
const SCOPE_BADGE: Record<NonNullable<Props["scope"]>, { icon: LucideIcon; className: string }> = {
|
||||||
org: { label: "Organization", icon: OrgIcon },
|
org: { className: "text-org", icon: OrgIcon },
|
||||||
[ProjectType.SecretManager]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.SecretManager]: { className: "text-project", icon: ProjectIcon },
|
||||||
[ProjectType.CertificateManager]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.CertificateManager]: { className: "text-project", icon: ProjectIcon },
|
||||||
[ProjectType.SSH]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.SSH]: { className: "text-project", icon: ProjectIcon },
|
||||||
[ProjectType.KMS]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.KMS]: { className: "text-project", icon: ProjectIcon },
|
||||||
[ProjectType.PAM]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.PAM]: { className: "text-project", icon: ProjectIcon },
|
||||||
[ProjectType.SecretScanning]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.SecretScanning]: { className: "text-project", icon: ProjectIcon },
|
||||||
namespace: { label: "Sub-Organization", icon: SubOrgIcon },
|
namespace: { className: "text-sub-org", icon: SubOrgIcon },
|
||||||
instance: { label: "Server", icon: InstanceIcon }
|
instance: { className: "text-neutral", icon: InstanceIcon }
|
||||||
};
|
|
||||||
|
|
||||||
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, TBadgeProps["variant"]> = {
|
|
||||||
org: "org",
|
|
||||||
[ProjectType.SecretManager]: "project",
|
|
||||||
[ProjectType.CertificateManager]: "project",
|
|
||||||
[ProjectType.SSH]: "project",
|
|
||||||
[ProjectType.KMS]: "project",
|
|
||||||
[ProjectType.PAM]: "project",
|
|
||||||
[ProjectType.SecretScanning]: "project",
|
|
||||||
namespace: "sub-org",
|
|
||||||
instance: "neutral"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
|
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
|
||||||
<div className={twMerge("mb-10 w-full", className)}>
|
<div className={twMerge("mb-10 w-full", className)}>
|
||||||
<div className="flex w-full justify-between">
|
<div className="flex w-full justify-between">
|
||||||
<div className="mr-4 flex w-full items-center">
|
<div className="mr-4 flex w-full items-center">
|
||||||
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
|
<h1
|
||||||
{scope && (
|
className={twMerge(
|
||||||
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5">
|
"text-3xl font-medium text-white capitalize underline decoration-2 underline-offset-4",
|
||||||
{createElement(SCOPE_NAME[scope].icon)}
|
scope === "org" && "decoration-org/90",
|
||||||
{SCOPE_NAME[scope].label}
|
scope === "instance" && "decoration-neutral/90",
|
||||||
</Badge>
|
scope === "namespace" && "decoration-sub-org/90",
|
||||||
|
Object.values(ProjectType).includes((scope as ProjectType) ?? "") &&
|
||||||
|
"decoration-project/90",
|
||||||
|
!scope && "no-underline"
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
{scope &&
|
||||||
|
createElement(SCOPE_BADGE[scope].icon, {
|
||||||
|
size: 26,
|
||||||
|
className: twMerge(SCOPE_BADGE[scope].className, "mr-3 mb-1 inline-block")
|
||||||
|
})}
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">{children}</div>
|
<div className="flex items-center gap-2">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export const Tab = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex h-10 cursor-pointer items-center justify-center border-transparent",
|
"flex h-11 cursor-pointer items-center justify-center border-transparent",
|
||||||
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-400 transition-all select-none",
|
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-300/75 transition-all select-none",
|
||||||
"data-[orientation=vertical]:xl:h-5 data-[orientation=vertical]:xl:border-b-0 data-[orientation=vertical]:xl:border-l",
|
"data-[orientation=vertical]:xl:h-5 data-[orientation=vertical]:xl:border-b-0 data-[orientation=vertical]:xl:border-l",
|
||||||
"border-b hover:text-mineshaft-200",
|
"border-b hover:text-mineshaft-200",
|
||||||
"data-[state=active]:border-mineshaft-400 data-[state=active]:text-white",
|
"data-[state=active]:border-mineshaft-400 data-[state=active]:text-white",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/b
|
|||||||
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection";
|
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection";
|
||||||
import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection";
|
import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection";
|
||||||
import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean";
|
import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean";
|
||||||
|
import { DNSMadeEasyConnectionMethod } from "@app/hooks/api/appConnections/types/dns-made-easy-connection";
|
||||||
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
|
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
|
||||||
import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection";
|
import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection";
|
||||||
import { NetlifyConnectionMethod } from "@app/hooks/api/appConnections/types/netlify-connection";
|
import { NetlifyConnectionMethod } from "@app/hooks/api/appConnections/types/netlify-connection";
|
||||||
@@ -111,6 +112,7 @@ export const APP_CONNECTION_MAP: Record<
|
|||||||
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
|
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
|
||||||
[AppConnection.GitLab]: { name: "GitLab", image: "GitLab.png" },
|
[AppConnection.GitLab]: { name: "GitLab", image: "GitLab.png" },
|
||||||
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
|
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
|
||||||
|
[AppConnection.DNSMadeEasy]: { name: "DNS Made Easy", image: "DNSMadeEasy.svg", size: 120 },
|
||||||
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
|
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
|
||||||
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
|
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
|
||||||
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
|
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
|
||||||
@@ -214,6 +216,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
return { name: "Client Secret", icon: faKey };
|
return { name: "Client Secret", icon: faKey };
|
||||||
case AzureClientSecretsConnectionMethod.Certificate:
|
case AzureClientSecretsConnectionMethod.Certificate:
|
||||||
return { name: "Certificate", icon: faCertificate };
|
return { name: "Certificate", icon: faCertificate };
|
||||||
|
case DNSMadeEasyConnectionMethod.APIKeySecret:
|
||||||
|
return { name: "API Key & Secret", icon: faKey };
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./queries";
|
||||||
|
export * from "./types";
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { appConnectionKeys } from "../queries";
|
||||||
|
import { TDNSMadeEasyZone } from "./types";
|
||||||
|
|
||||||
|
const dnsMadeEasyConnectionKeys = {
|
||||||
|
all: [...appConnectionKeys.all, "dns-made-easy"] as const,
|
||||||
|
listZones: (connectionId: string) =>
|
||||||
|
[...dnsMadeEasyConnectionKeys.all, "zones", connectionId] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDNSMadeEasyConnectionListZones = (
|
||||||
|
connectionId: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TDNSMadeEasyZone[],
|
||||||
|
unknown,
|
||||||
|
TDNSMadeEasyZone[],
|
||||||
|
ReturnType<typeof dnsMadeEasyConnectionKeys.listZones>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: dnsMadeEasyConnectionKeys.listZones(connectionId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TDNSMadeEasyZone[]>(
|
||||||
|
`/api/v1/app-connections/dns-made-easy/${connectionId}/dns-made-easy-zones`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type TDNSMadeEasyZone = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
@@ -29,6 +29,7 @@ export enum AppConnection {
|
|||||||
Flyio = "flyio",
|
Flyio = "flyio",
|
||||||
GitLab = "gitlab",
|
GitLab = "gitlab",
|
||||||
Cloudflare = "cloudflare",
|
Cloudflare = "cloudflare",
|
||||||
|
DNSMadeEasy = "dns-made-easy",
|
||||||
Bitbucket = "bitbucket",
|
Bitbucket = "bitbucket",
|
||||||
Zabbix = "zabbix",
|
Zabbix = "zabbix",
|
||||||
Railway = "railway",
|
Railway = "railway",
|
||||||
|
|||||||
@@ -184,6 +184,10 @@ export type TRedisConnectionOption = TAppConnectionOptionBase & {
|
|||||||
app: AppConnection.Redis;
|
app: AppConnection.Redis;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TDNSMadeEasyConnectionOption = TAppConnectionOptionBase & {
|
||||||
|
app: AppConnection.DNSMadeEasy;
|
||||||
|
};
|
||||||
|
|
||||||
export type TAppConnectionOption =
|
export type TAppConnectionOption =
|
||||||
| TAwsConnectionOption
|
| TAwsConnectionOption
|
||||||
| TGitHubConnectionOption
|
| TGitHubConnectionOption
|
||||||
@@ -225,7 +229,8 @@ export type TAppConnectionOption =
|
|||||||
| TOktaConnectionOption
|
| TOktaConnectionOption
|
||||||
| TAzureAdCsConnectionOption
|
| TAzureAdCsConnectionOption
|
||||||
| TLaravelForgeConnectionOption
|
| TLaravelForgeConnectionOption
|
||||||
| TChefConnectionOption;
|
| TChefConnectionOption
|
||||||
|
| TDNSMadeEasyConnectionOption;
|
||||||
|
|
||||||
export type TAppConnectionOptionMap = {
|
export type TAppConnectionOptionMap = {
|
||||||
[AppConnection.AWS]: TAwsConnectionOption;
|
[AppConnection.AWS]: TAwsConnectionOption;
|
||||||
@@ -257,6 +262,7 @@ export type TAppConnectionOptionMap = {
|
|||||||
[AppConnection.Flyio]: TFlyioConnectionOption;
|
[AppConnection.Flyio]: TFlyioConnectionOption;
|
||||||
[AppConnection.GitLab]: TGitlabConnectionOption;
|
[AppConnection.GitLab]: TGitlabConnectionOption;
|
||||||
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
|
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
|
||||||
|
[AppConnection.DNSMadeEasy]: TDNSMadeEasyConnectionOption;
|
||||||
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
||||||
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
||||||
[AppConnection.Railway]: TRailwayConnectionOption;
|
[AppConnection.Railway]: TRailwayConnectionOption;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||||
|
|
||||||
|
export enum DNSMadeEasyConnectionMethod {
|
||||||
|
APIKeySecret = "api-key-secret"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TDNSMadeEasyConnection = TRootAppConnection & { app: AppConnection.DNSMadeEasy } & {
|
||||||
|
method: DNSMadeEasyConnectionMethod.APIKeySecret;
|
||||||
|
credentials: {
|
||||||
|
apiKey: string;
|
||||||
|
secretKey: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@ import { TChefConnection } from "./chef-connection";
|
|||||||
import { TCloudflareConnection } from "./cloudflare-connection";
|
import { TCloudflareConnection } from "./cloudflare-connection";
|
||||||
import { TDatabricksConnection } from "./databricks-connection";
|
import { TDatabricksConnection } from "./databricks-connection";
|
||||||
import { TDigitalOceanConnection } from "./digital-ocean";
|
import { TDigitalOceanConnection } from "./digital-ocean";
|
||||||
|
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection";
|
||||||
import { TFlyioConnection } from "./flyio-connection";
|
import { TFlyioConnection } from "./flyio-connection";
|
||||||
import { TGcpConnection } from "./gcp-connection";
|
import { TGcpConnection } from "./gcp-connection";
|
||||||
import { TGitHubConnection } from "./github-connection";
|
import { TGitHubConnection } from "./github-connection";
|
||||||
@@ -57,6 +58,7 @@ export * from "./checkly-connection";
|
|||||||
export * from "./chef-connection";
|
export * from "./chef-connection";
|
||||||
export * from "./cloudflare-connection";
|
export * from "./cloudflare-connection";
|
||||||
export * from "./databricks-connection";
|
export * from "./databricks-connection";
|
||||||
|
export * from "./dns-made-easy-connection";
|
||||||
export * from "./flyio-connection";
|
export * from "./flyio-connection";
|
||||||
export * from "./gcp-connection";
|
export * from "./gcp-connection";
|
||||||
export * from "./github-connection";
|
export * from "./github-connection";
|
||||||
@@ -127,7 +129,8 @@ export type TAppConnection =
|
|||||||
| TNorthflankConnection
|
| TNorthflankConnection
|
||||||
| TOktaConnection
|
| TOktaConnection
|
||||||
| TRedisConnection
|
| TRedisConnection
|
||||||
| TChefConnection;
|
| TChefConnection
|
||||||
|
| TDNSMadeEasyConnection;
|
||||||
|
|
||||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
|
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
|||||||
|
|
||||||
export const ACME_DNS_PROVIDER_NAME_MAP: Record<AcmeDnsProvider, string> = {
|
export const ACME_DNS_PROVIDER_NAME_MAP: Record<AcmeDnsProvider, string> = {
|
||||||
[AcmeDnsProvider.ROUTE53]: "Route53",
|
[AcmeDnsProvider.ROUTE53]: "Route53",
|
||||||
[AcmeDnsProvider.Cloudflare]: "Cloudflare"
|
[AcmeDnsProvider.Cloudflare]: "Cloudflare",
|
||||||
|
[AcmeDnsProvider.DNSMadeEasy]: "DNS Made Easy"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppConnection> = {
|
export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppConnection> = {
|
||||||
[AcmeDnsProvider.ROUTE53]: AppConnection.AWS,
|
[AcmeDnsProvider.ROUTE53]: AppConnection.AWS,
|
||||||
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare
|
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare,
|
||||||
|
[AcmeDnsProvider.DNSMadeEasy]: AppConnection.DNSMadeEasy
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export enum CaRenewalType {
|
|||||||
|
|
||||||
export enum AcmeDnsProvider {
|
export enum AcmeDnsProvider {
|
||||||
ROUTE53 = "route53",
|
ROUTE53 = "route53",
|
||||||
Cloudflare = "cloudflare"
|
Cloudflare = "cloudflare",
|
||||||
|
DNSMadeEasy = "dns-made-easy"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CaCapability {
|
export enum CaCapability {
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ export {
|
|||||||
useGetProfileCertificates,
|
useGetProfileCertificates,
|
||||||
useListCertificateProfiles
|
useListCertificateProfiles
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
export type * from "./types";
|
export * from "./types";
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
|
export enum EnrollmentType {
|
||||||
|
API = "api",
|
||||||
|
EST = "est",
|
||||||
|
ACME = "acme"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum IssuerType {
|
||||||
|
CA = "ca",
|
||||||
|
SELF_SIGNED = "self-signed"
|
||||||
|
}
|
||||||
|
|
||||||
export type TCertificateProfile = {
|
export type TCertificateProfile = {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
caId: string;
|
caId: string | null;
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
enrollmentType: "api" | "est" | "acme";
|
enrollmentType: EnrollmentType;
|
||||||
|
issuerType: IssuerType;
|
||||||
estConfigId?: string;
|
estConfigId?: string;
|
||||||
apiConfigId?: string;
|
apiConfigId?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -44,11 +56,12 @@ export type TCertificateProfileWithDetails = TCertificateProfile & {
|
|||||||
|
|
||||||
export type TCreateCertificateProfileDTO = {
|
export type TCreateCertificateProfileDTO = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
caId: string;
|
caId?: string;
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
enrollmentType: "api" | "est" | "acme";
|
enrollmentType: EnrollmentType;
|
||||||
|
issuerType: IssuerType;
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
passphrase: string;
|
passphrase: string;
|
||||||
@@ -65,6 +78,8 @@ export type TUpdateCertificateProfileDTO = {
|
|||||||
profileId: string;
|
profileId: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
@@ -87,7 +102,9 @@ export type TListCertificateProfilesDTO = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
includeConfigs?: boolean;
|
includeConfigs?: boolean;
|
||||||
enrollmentType?: "api" | "est" | "acme";
|
enrollmentType?: EnrollmentType;
|
||||||
|
issuerType?: IssuerType;
|
||||||
|
caId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetCertificateProfileByIdDTO = {
|
export type TGetCertificateProfileByIdDTO = {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@import "@fontsource/inter/400.css" layer(base);
|
|
||||||
@import "@fontsource/inter/500.css" layer(base);
|
|
||||||
@import "@fontsource/inter/700.css" layer(base);
|
|
||||||
|
|
||||||
@source not "../public";
|
@source not "../public";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export const KmsLayout = () => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
|
import { Link, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
|
||||||
import { UserPlusIcon } from "lucide-react";
|
import { ChevronRight, UserPlusIcon } from "lucide-react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Mfa } from "@app/components/auth/Mfa";
|
import { Mfa } from "@app/components/auth/Mfa";
|
||||||
@@ -31,7 +31,6 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import {
|
import {
|
||||||
BreadcrumbContainer,
|
|
||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -43,7 +42,6 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
TBreadcrumbFormat,
|
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
|
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
|
||||||
@@ -69,6 +67,7 @@ import { MfaMethod } from "@app/hooks/api/auth/types";
|
|||||||
import { getAuthToken } from "@app/hooks/api/reactQuery";
|
import { getAuthToken } from "@app/hooks/api/reactQuery";
|
||||||
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
|
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
|
||||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
|
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
|
||||||
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
||||||
|
|
||||||
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
||||||
@@ -192,9 +191,6 @@ export const Navbar = () => {
|
|||||||
}
|
}
|
||||||
}, [subscription, isBillingPage, isModalIntrusive]);
|
}, [subscription, isBillingPage, isModalIntrusive]);
|
||||||
|
|
||||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
|
||||||
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
|
|
||||||
|
|
||||||
const handleOrgChange = async (orgId: string, onSuccess?: () => void | Promise<void>) => {
|
const handleOrgChange = async (orgId: string, onSuccess?: () => void | Promise<void>) => {
|
||||||
if (orgId === currentOrg.id) return;
|
if (orgId === currentOrg.id) return;
|
||||||
|
|
||||||
@@ -316,7 +312,9 @@ export const Navbar = () => {
|
|||||||
|
|
||||||
const isServerAdminPanel = location.pathname.startsWith("/admin");
|
const isServerAdminPanel = location.pathname.startsWith("/admin");
|
||||||
|
|
||||||
const isProjectScope = location.pathname.startsWith(`/organizations/${currentOrg.id}/projects`);
|
const isProjectScope =
|
||||||
|
location.pathname.startsWith(`/organizations/${currentOrg.id}/projects`) &&
|
||||||
|
location.pathname !== `/organizations/${currentOrg.id}/projects`;
|
||||||
|
|
||||||
const handleOrgNav = async (org: Organization) => {
|
const handleOrgNav = async (org: Organization) => {
|
||||||
if (currentOrg?.id === org.id) return;
|
if (currentOrg?.id === org.id) return;
|
||||||
@@ -346,16 +344,15 @@ export const Navbar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1">
|
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4">
|
||||||
<div className="mr-auto flex items-center overflow-hidden">
|
<div className="mr-auto flex h-full min-w-34 items-center">
|
||||||
<div className="shrink-0">
|
<div className="mt-0.5 shrink-0">
|
||||||
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
|
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
|
||||||
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
|
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
<ChevronRight size={18} className="mx-3 mt-[3px] text-mineshaft-400/70" />
|
||||||
{isServerAdminPanel ? (
|
{isServerAdminPanel ? (
|
||||||
<>
|
|
||||||
<Link
|
<Link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
|
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
|
||||||
@@ -363,44 +360,43 @@ export const Navbar = () => {
|
|||||||
<InstanceIcon className="size-3.5 text-xs text-bunker-300" />
|
<InstanceIcon className="size-3.5 text-xs text-bunker-300" />
|
||||||
<div className="whitespace-nowrap">Server Console</div>
|
<div className="whitespace-nowrap">Server Console</div>
|
||||||
</Link>
|
</Link>
|
||||||
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
|
|
||||||
{breadcrumbs ? (
|
|
||||||
// scott: remove /admin as we show server console above
|
|
||||||
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex min-w-12 items-center overflow-hidden">
|
<div
|
||||||
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
|
|
||||||
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
|
|
||||||
<Badge
|
|
||||||
asChild
|
|
||||||
variant="org"
|
|
||||||
isTruncatable
|
|
||||||
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"gap-x-1.5 text-sm",
|
"relative flex min-w-16 items-center self-end rounded-t-md border-x border-t pt-1.5 pr-2 pb-2.5 pl-3",
|
||||||
(isProjectScope || isSubOrganization) &&
|
!isProjectScope && !isSubOrganization
|
||||||
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-org"
|
? "border-org/15 bg-gradient-to-b from-org/10 to-org/[0.075]"
|
||||||
|
: "border-transparent"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/* scott: the below is used to hide the top border from the org nav bar */}
|
||||||
|
{!isProjectScope && !isSubOrganization && (
|
||||||
|
<div className="absolute -bottom-px left-0 h-px w-full bg-mineshaft-900">
|
||||||
|
<div className="h-full bg-org/[0.075]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
|
||||||
|
<div className="group mr-1 flex min-w-0 cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100">
|
||||||
<button
|
<button
|
||||||
|
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
handleOrgChange(rootOrg.id);
|
navigate({
|
||||||
|
to: "/organizations/$orgId/projects",
|
||||||
|
params: { orgId: currentOrg.id }
|
||||||
|
});
|
||||||
if (isSubOrganization) {
|
if (isSubOrganization) {
|
||||||
await router.invalidate({ sync: true }).catch(() => null);
|
await router.invalidate({ sync: true }).catch(() => null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<OrgIcon className="size-[12px]" />
|
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
|
||||||
<span>{rootOrg?.name}</span>
|
<span className="truncate">{rootOrg?.name}</span>
|
||||||
</button>
|
<Badge variant="org" className="hidden lg:inline-flex">
|
||||||
|
Organization
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className="mr-1 hidden rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline! md:inline-block">
|
</button>
|
||||||
{getPlan(subscription)}
|
|
||||||
</div>
|
|
||||||
{subscription.cardDeclined && (
|
{subscription.cardDeclined && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
|
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
|
||||||
@@ -595,19 +591,15 @@ export const Navbar = () => {
|
|||||||
)}
|
)}
|
||||||
{isProjectScope && (
|
{isProjectScope && (
|
||||||
<>
|
<>
|
||||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
<ChevronRight size={18} className="mx-3 mt-[3px] text-mineshaft-400/70" />
|
||||||
{breadcrumbs ? (
|
<ProjectSelect />
|
||||||
<BreadcrumbContainer
|
|
||||||
className="min-w-[15rem] flex-1"
|
|
||||||
breadcrumbs={[breadcrumbs[0]] as TBreadcrumbFormat[]}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
|
||||||
|
{subscription && subscription.slug === "starter" && !subscription.has_used_trial ? (
|
||||||
<Tooltip content="Start Free Pro Trial">
|
<Tooltip content="Start Free Pro Trial">
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -628,6 +620,10 @@ export const Navbar = () => {
|
|||||||
Free Pro Trial
|
Free Pro Trial
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<div className="mt-0.5 mr-3 hidden rounded-sm border border-mineshaft-400 px-1 text-xs text-mineshaft-100 no-underline! opacity-50 md:inline-block">
|
||||||
|
{getPlan(subscription)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* eslint-disable-next-line no-nested-ternary */}
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
{!location.pathname.startsWith("/admin") ? (
|
{!location.pathname.startsWith("/admin") ? (
|
||||||
@@ -653,7 +649,7 @@ export const Navbar = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UserPlusIcon className="inline-block size-3.5" />
|
<UserPlusIcon className="inline-block size-3.5" />
|
||||||
<span className="ml-2 hidden md:inline-block">Invite Members</span>
|
<span className="ml-2 hidden md:inline-block">Invite Users</span>
|
||||||
</Link>
|
</Link>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
@@ -758,7 +754,7 @@ export const Navbar = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUserPlus} />}>
|
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUserPlus} />}>
|
||||||
Invite Members
|
Invite Users
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export const OrgNavBar = ({ isHidden }: Props) => {
|
|||||||
const variant = isRootOrganization ? "org" : "namespace";
|
const variant = isRootOrganization ? "org" : "namespace";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="bg-mineshaft-900">
|
||||||
{!isHidden && (
|
{!isHidden && (
|
||||||
<div className="dark flex w-full flex-col overflow-x-hidden border-b border-mineshaft-600 bg-mineshaft-900 px-4">
|
<div className="dark flex w-full flex-col overflow-x-hidden border-y border-t-org/15 border-b-org/5 bg-gradient-to-b from-org/[0.075] to-org/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-org-items"
|
key="menu-org-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
@@ -114,6 +114,6 @@ export const OrgNavBar = ({ isHidden }: Props) => {
|
|||||||
isOpen={popUp?.createOrg?.isOpen}
|
isOpen={popUp?.createOrg?.isOpen}
|
||||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export const PamLayout = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export const PkiManagerLayout = () => {
|
|||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
return (
|
return (
|
||||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
faStar as faSolidStar
|
faStar as faSolidStar
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Link, linkOptions } from "@tanstack/react-router";
|
import { Link, linkOptions, useParams } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
@@ -46,7 +46,7 @@ const PROJECT_TYPE_NAME: Record<ProjectType, string> = {
|
|||||||
[ProjectType.SecretScanning]: "Secret Scanning"
|
[ProjectType.SecretScanning]: "Secret Scanning"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectSelect = () => {
|
const ProjectSelectInner = () => {
|
||||||
const [searchProject, setSearchProject] = useState("");
|
const [searchProject, setSearchProject] = useState("");
|
||||||
const { currentProject: currentWorkspace } = useProject();
|
const { currentProject: currentWorkspace } = useProject();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
@@ -92,7 +92,11 @@ export const ProjectSelect = () => {
|
|||||||
}, [projects, projectFavorites, currentWorkspace]);
|
}, [projects, projectFavorites, currentWorkspace]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mr-2 flex items-center gap-1 overflow-hidden">
|
<div className="relative mr-2 flex min-w-16 items-center gap-1 self-end rounded-t-md border-x border-t border-project/10 bg-gradient-to-b from-project/10 to-project/[0.075] pt-1.5 pr-1 pb-2.5 pl-3">
|
||||||
|
{/* scott: the below is used to hide the top border from the org nav bar */}
|
||||||
|
<div className="absolute -bottom-px left-0 h-px w-full bg-mineshaft-900">
|
||||||
|
<div className="h-full bg-project/[0.075]" />
|
||||||
|
</div>
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<Link
|
<Link
|
||||||
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
|
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
|
||||||
@@ -100,16 +104,12 @@ export const ProjectSelect = () => {
|
|||||||
projectId: currentWorkspace.id,
|
projectId: currentWorkspace.id,
|
||||||
orgId: currentWorkspace.orgId
|
orgId: currentWorkspace.orgId
|
||||||
}}
|
}}
|
||||||
className="group flex cursor-pointer items-center gap-x-1.5 overflow-hidden hover:text-white"
|
className="group flex cursor-pointer items-center gap-x-2 overflow-hidden pt-0.5 text-sm text-white"
|
||||||
>
|
>
|
||||||
<p className="inline-block truncate text-mineshaft-200 group-hover:underline">
|
<ProjectIcon className="size-[14px] shrink-0 text-project" />
|
||||||
{currentWorkspace?.name}
|
<span className="truncate">{currentWorkspace?.name}</span>
|
||||||
</p>
|
<Badge variant="project" className="hidden lg:inline-flex">
|
||||||
<Badge variant="project">
|
|
||||||
<ProjectIcon />
|
|
||||||
<span className="hidden sm:inline-block">
|
|
||||||
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
||||||
</span>
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</Link>
|
</Link>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -118,7 +118,7 @@ export const ProjectSelect = () => {
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
ariaLabel="switch-project"
|
ariaLabel="switch-project"
|
||||||
className="px-2 py-1"
|
className="top-px px-2 py-1"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -238,3 +238,14 @@ export const ProjectSelect = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ProjectSelect = () => {
|
||||||
|
const params = useParams({ strict: false });
|
||||||
|
|
||||||
|
// Return null during navigation when projectId is not available
|
||||||
|
if (!params.projectId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ProjectSelectInner />;
|
||||||
|
};
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ export const SecretManagerLayout = () => {
|
|||||||
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
|
|||||||