Merge branch 'main' of https://github.com/Infisical/infisical into aws-lambda-secret-sync-docs

This commit is contained in:
Piyush Gupta
2025-11-26 22:19:01 +05:30
538 changed files with 9140 additions and 5388 deletions

View File

@@ -34,6 +34,7 @@ ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV VITE_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG CAPTCHA_SITE_KEY
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY

View File

@@ -1,6 +1,7 @@
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
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
@@ -92,9 +93,7 @@ Feature: External CA
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
<subject>
"""
# Pebble has a strict rule to only takes SANs
Then I add subject alternative name to certificate signing request csr
@@ -177,4 +176,196 @@ Feature: External CA
[
"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"} |
| {} |

View File

@@ -0,0 +1,33 @@
Feature: Internal CA
Scenario: CSR with SANs only
Given I have an ACME cert profile 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
"""
{}
"""
And 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
And 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 ".subject.common_name" should be equal to null
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
"""
[
"localhost"
]
"""

View File

@@ -147,6 +147,40 @@ def step_impl(context: Context, var_name: str):
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}")
def step_impl(context: Context, var_name: str):
jwt_token = context.vars["AUTH_TOKEN"]

View File

@@ -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");
});
}
}

View File

@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const PkiCertificateProfilesSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
caId: z.string().uuid(),
caId: z.string().uuid().nullable().optional(),
certificateTemplateId: z.string().uuid(),
slug: z.string(),
description: z.string().nullable().optional(),
@@ -19,7 +19,8 @@ export const PkiCertificateProfilesSchema = z.object({
apiConfigId: z.string().uuid().nullable().optional(),
createdAt: 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>;

View File

@@ -158,6 +158,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
},
data: {
...req.body,
name: req.body.slug,
...req.body.type,
permissions: req.body.permissions || undefined
}

View File

@@ -243,7 +243,7 @@ export const accessApprovalRequestServiceFactory = ({
);
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const projectPath = `/projects/secret-management/${project.id}`;
const projectPath = `/organizations/${project.orgId}/projects/secret-management/${project.id}`;
const approvalPath = `${projectPath}/approval`;
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
@@ -399,7 +399,7 @@ export const accessApprovalRequestServiceFactory = ({
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const editorFullName = `${editedByUser.firstName} ${editedByUser.lastName}`;
const projectPath = `/projects/secret-management/${project.id}`;
const projectPath = `/organizations/${project.orgId}/projects/secret-management/${project.id}`;
const approvalPath = `${projectPath}/approval`;
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
@@ -766,7 +766,7 @@ export const accessApprovalRequestServiceFactory = ({
.map((appUser) => appUser.email)
.filter((email): email is string => !!email);
const approvalPath = `/projects/secret-management/${project.id}/approval`;
const approvalPath = `/organizations/${project.orgId}/projects/secret-management/${project.id}/approval`;
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
await notificationService.createUserNotifications(

View File

@@ -2787,6 +2787,7 @@ interface CreateCertificateProfile {
name: string;
projectId: string;
enrollmentType: string;
issuerType: string;
};
}

View File

@@ -450,8 +450,8 @@ export const licenseServiceFactory = ({
} = await licenseServerCloudApi.request.post(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`,
{
success_url: `${envConfig.SITE_URL}/organization/billing`,
cancel_url: `${envConfig.SITE_URL}/organization/billing`
success_url: `${envConfig.SITE_URL}/organizations/${orgId}/billing`,
cancel_url: `${envConfig.SITE_URL}/organizations/${orgId}/billing`
}
);
@@ -464,7 +464,7 @@ export const licenseServiceFactory = ({
} = await licenseServerCloudApi.request.post(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`,
{
return_url: `${envConfig.SITE_URL}/organization/billing`
return_url: `${envConfig.SITE_URL}/organizations/${orgId}/billing`
}
);

View File

@@ -1,3 +1,5 @@
import axios, { AxiosError } from "axios";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isPrivateIp } from "@app/lib/ip/ipRange";
@@ -13,10 +15,6 @@ import {
import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas";
import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types";
type FetchError = Error & {
code?: string;
};
type TPkiAcmeChallengeServiceFactoryDep = {
acmeChallengeDAL: Pick<
TPkiAcmeChallengeDALFactory,
@@ -74,18 +72,20 @@ export const pkiAcmeChallengeServiceFactory = ({
// Notice: well, we are in a transaction, ideally we should not hold transaction and perform
// a long running operation for long time. But assuming we are not performing a tons of
// challenge validation at the same time, it should be fine.
const challengeResponse = await fetch(challengeUrl, {
const challengeResponse = await axios.get<string>(challengeUrl.toString(), {
// In case if we override the host in the development mode, still provide the original host in the header
// to help the upstream server to validate the request
headers: { Host: host },
signal: AbortSignal.timeout(timeoutMs)
headers: { Host: challenge.auth.identifierValue },
timeout: timeoutMs,
responseType: "text",
validateStatus: () => true
});
if (challengeResponse.status !== 200) {
throw new AcmeIncorrectResponseError({
message: `ACME challenge response is not 200: ${challengeResponse.status}`
});
}
const challengeResponseBody = await challengeResponse.text();
const challengeResponseBody: string = challengeResponse.data;
const thumbprint = challenge.auth.account.publicKeyThumbprint;
const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`;
if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) {
@@ -96,35 +96,25 @@ export const pkiAcmeChallengeServiceFactory = ({
// TODO: we should retry the challenge validation a few times, but let's keep it simple for now
await acmeChallengeDAL.markAsInvalidCascadeById(challengeId, tx);
// Properly type and inspect the error
if (exp instanceof TypeError && exp.message.includes("fetch failed")) {
const { cause } = exp;
let errors: Error[] = [];
if (cause instanceof AggregateError) {
errors = cause.errors as Error[];
} else if (cause instanceof Error) {
errors = [cause];
if (axios.isAxiosError(exp)) {
const axiosError = exp as AxiosError;
const errorCode = axiosError.code;
const errorMessage = axiosError.message;
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
return new AcmeConnectionError({ message: "Connection refused" });
}
// eslint-disable-next-line no-unreachable-loop
for (const err of errors) {
// TODO: handle multiple errors, return a compound error instead of just the first error
const fetchError = err as FetchError;
if (fetchError.code === "ECONNREFUSED" || fetchError.message.includes("ECONNREFUSED")) {
return new AcmeConnectionError({ message: "Connection refused" });
}
if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) {
return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
}
logger.error(exp, "Unknown error validating ACME challenge response");
return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) {
return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
}
} else if (exp instanceof DOMException) {
if (exp.name === "TimeoutError") {
if (errorCode === "ECONNABORTED" || errorMessage.includes("timeout")) {
logger.error(exp, "Connection timed out while validating ACME challenge response");
return new AcmeConnectionError({ message: "Connection timed out" });
}
logger.error(exp, "Unknown error validating ACME challenge response");
return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
} else if (exp instanceof Error) {
}
if (exp instanceof Error) {
logger.error(exp, "Error validating ACME challenge response");
} else {
logger.error(exp, "Unknown error validating ACME challenge response");

View File

@@ -683,6 +683,13 @@ export const pkiAcmeServiceFactory = ({
payload: TFinalizeAcmeOrderPayload;
}): Promise<TAcmeResponse<TAcmeOrderResource>> => {
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);
if (!order) {
throw new NotFoundError({ message: "ACME order not found" });
@@ -703,9 +710,6 @@ export const pkiAcmeServiceFactory = ({
// Check and validate the CSR
const certificateRequest = extractCertificateRequestFromCSR(csr);
if (!certificateRequest.commonName) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name is required" });
}
if (
certificateRequest.subjectAlternativeNames?.some(
(san) => san.type !== CertSubjectAlternativeNameType.DNS_NAME
@@ -721,7 +725,7 @@ export const pkiAcmeServiceFactory = ({
const csrIdentifierValues = new Set(
(certificateRequest.subjectAlternativeNames ?? [])
.map((san) => san.value.toLowerCase())
.concat([certificateRequest.commonName.toLowerCase()])
.concat(certificateRequest.commonName ? [certificateRequest.commonName.toLowerCase()] : [])
);
if (
csrIdentifierValues.size !== orderWithAuthorizations.authorizations.length ||
@@ -732,7 +736,7 @@ export const pkiAcmeServiceFactory = ({
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) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
@@ -772,7 +776,9 @@ export const pkiAcmeServiceFactory = ({
const cert = await orderCertificate(
{
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),
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

View File

@@ -37,7 +37,7 @@ export const sendApprovalEmailsFn = async ({
type: NotificationType.SECRET_CHANGE_REQUEST,
title: "Secret Change Request",
body: `You have a new secret change request pending your review for the project **${project.name}** in the organization **${project.organization.name}**.`,
link: `/projects/secret-management/${project.id}/approval`
link: `/organizations/${project.orgId}/projects/secret-management/${project.id}/approval`
}))
);
@@ -51,7 +51,7 @@ export const sendApprovalEmailsFn = async ({
firstName: reviewerUser.firstName,
projectName: project.name,
organizationName: project.organization.name,
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval}`
approvalUrl: `${cfg.SITE_URL}/organizations/${project.orgId}/projects/secret-management/${project.id}/approval}`
},
template: SmtpTemplates.SecretApprovalRequestNeedsReview
});

View File

@@ -1037,7 +1037,7 @@ export const secretApprovalRequestServiceFactory = ({
bypassReason,
secretPath: policy.secretPath,
environment: env.name,
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`
approvalUrl: `${cfg.SITE_URL}/organizations/${project.orgId}/projects/secret-management/${project.id}/approval`
},
template: SmtpTemplates.AccessSecretRequestBypassed
});
@@ -1416,7 +1416,7 @@ export const secretApprovalRequestServiceFactory = ({
const env = await projectEnvDAL.findOne({ id: policy.envId });
const user = await userDAL.findById(actorId);
const projectPath = `/projects/secret-management/${projectId}`;
const projectPath = `/organizations/${actorOrgId}/projects/secret-management/${projectId}`;
const approvalPath = `${projectPath}/approval`;
const cfg = getConfig();
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
@@ -1792,7 +1792,7 @@ export const secretApprovalRequestServiceFactory = ({
const user = await userDAL.findById(actorId);
const env = await projectEnvDAL.findOne({ id: policy.envId });
const projectPath = `/projects/secret-management/${project.id}`;
const projectPath = `/organizations/${actorOrgId}/projects/secret-management/${project.id}`;
const approvalPath = `${projectPath}/approval`;
const cfg = getConfig();
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;

View File

@@ -156,7 +156,7 @@ export const secretRotationV2QueueServiceFactory = async ({
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
const rotationPath = `/projects/secret-management/${projectId}/secrets/${environment.slug}`;
const rotationPath = `/organizations/${project.orgId}/projects/secret-management/${projectId}/secrets/${environment.slug}`;
await notificationService.createUserNotifications(
projectAdmins.map((admin) => ({

View File

@@ -637,7 +637,7 @@ export const secretScanningV2QueueServiceFactory = async ({
numberOfSecrets: payload.numberOfSecrets,
isDiffScan: payload.isDiffScan,
url: encodeURI(
`${appCfg.SITE_URL}/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
`${appCfg.SITE_URL}/organizations/${project.orgId}/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
),
timestamp
}
@@ -648,7 +648,7 @@ export const secretScanningV2QueueServiceFactory = async ({
timestamp,
errorMessage: payload.errorMessage,
url: encodeURI(
`${appCfg.SITE_URL}/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}`
`${appCfg.SITE_URL}/organizations/${project.orgId}/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}`
)
}
});

View File

@@ -119,6 +119,7 @@ const envSchema = z
})
.default("{}")
),
DNS_MADE_EASY_SANDBOX_ENABLED: zodStrBool.default("false").optional(),
// smtp options
SMTP_HOST: zpStr(z.string().optional()),
SMTP_IGNORE_TLS: zodStrBool.default("false"),

View File

@@ -43,7 +43,9 @@ export const registerServeUI = async (
const frontendPath = path.join(dir, frontendName);
await server.register(staticServe, {
root: frontendPath,
wildcard: false
wildcard: false,
maxAge: "30d",
immutable: true
});
server.route({
@@ -58,11 +60,12 @@ export const registerServeUI = async (
return;
}
// This should help avoid caching any chunks (temp fix)
void reply.header("Cache-Control", "no-cache, no-store, must-revalidate, private, max-age=0");
void reply.header("Pragma", "no-cache");
void reply.header("Expires", "0");
return reply.sendFile("index.html");
return reply.sendFile("index.html", {
immutable: false,
maxAge: 0,
lastModified: false,
etag: false
});
}
});
}

View File

@@ -2219,7 +2219,10 @@ export const registerRoutes = async (
permissionService,
certificateSyncDAL,
pkiSyncDAL,
pkiSyncQueue
pkiSyncQueue,
kmsService,
projectDAL,
certificateBodyDAL
});
const certificateV3Queue = certificateV3QueueServiceFactory({

View File

@@ -61,6 +61,10 @@ import {
DigitalOceanConnectionListItemSchema,
SanitizedDigitalOceanConnectionSchema
} 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 { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
@@ -170,7 +174,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedAzureADCSConnectionSchema.options,
...SanitizedRedisConnectionSchema.options,
...SanitizedLaravelForgeConnectionSchema.options,
...SanitizedChefConnectionSchema.options
...SanitizedChefConnectionSchema.options,
...SanitizedDNSMadeEasyConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -215,7 +220,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AzureADCSConnectionListItemSchema,
RedisConnectionListItemSchema,
LaravelForgeConnectionListItemSchema,
ChefConnectionListItemSchema
ChefConnectionListItemSchema,
DNSMadeEasyConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -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;
}
});
};

View File

@@ -16,6 +16,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
import { registerFlyioConnectionRouter } from "./flyio-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.GitLab]: registerGitLabConnectionRouter,
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
[AppConnection.DNSMadeEasy]: registerDNSMadeEasyConnectionRouter,
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter,

View File

@@ -8,7 +8,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { 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) => {
server.route({
@@ -23,7 +23,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
body: z
.object({
projectId: z.string().min(1),
caId: z.string().uuid(),
caId: z.string().uuid().optional(),
certificateTemplateId: z.string().uuid(),
slug: z
.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"),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType),
issuerType: z.nativeEnum(IssuerType).default(IssuerType.CA),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
@@ -50,43 +51,100 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (!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 !!data.estConfig;
}
return true;
},
{
message:
"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."
message: "EST enrollment type requires 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"
}
),
response: {
@@ -115,7 +173,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
certificateProfileId: certificateProfile.id,
name: certificateProfile.slug,
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),
search: z.string().optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
issuerType: z.nativeEnum(IssuerType).optional(),
caId: z.string().uuid().optional()
}),
response: {
@@ -339,6 +399,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
.optional(),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
issuerType: z.nativeEnum(IssuerType).optional(),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),

View File

@@ -29,6 +29,7 @@ export enum AppConnection {
Flyio = "flyio",
GitLab = "gitlab",
Cloudflare = "cloudflare",
DNSMadeEasy = "dns-made-easy",
Zabbix = "zabbix",
Railway = "railway",
Bitbucket = "bitbucket",

View File

@@ -88,6 +88,11 @@ import {
getDigitalOceanConnectionListItem,
validateDigitalOceanConnectionCredentials
} 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 { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
@@ -171,7 +176,8 @@ const PKI_APP_CONNECTIONS = [
AppConnection.Cloudflare,
AppConnection.AzureADCS,
AppConnection.AzureKeyVault,
AppConnection.Chef
AppConnection.Chef,
AppConnection.DNSMadeEasy
];
export const listAppConnectionOptions = (projectType?: ProjectType) => {
@@ -207,6 +213,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getFlyioConnectionListItem(),
getGitLabConnectionListItem(),
getCloudflareConnectionListItem(),
getDNSMadeEasyConnectionListItem(),
getZabbixConnectionListItem(),
getRailwayConnectionListItem(),
getBitbucketConnectionListItem(),
@@ -339,6 +346,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.DNSMadeEasy]: validateDNSMadeEasyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
@@ -395,6 +403,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case OktaConnectionMethod.ApiToken:
case LaravelForgeConnectionMethod.ApiToken:
return "API Token";
case DNSMadeEasyConnectionMethod.APIKeySecret:
return "API Key & Secret";
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
case MySqlConnectionMethod.UsernameAndPassword:
@@ -483,6 +493,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
[AppConnection.DNSMadeEasy]: platformManagedCredentialsNotSupported,
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,

View File

@@ -32,6 +32,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Flyio]: "Fly.io",
[AppConnection.GitLab]: "GitLab",
[AppConnection.Cloudflare]: "Cloudflare",
[AppConnection.DNSMadeEasy]: "DNS Made Easy",
[AppConnection.Zabbix]: "Zabbix",
[AppConnection.Railway]: "Railway",
[AppConnection.Bitbucket]: "Bitbucket",
@@ -77,6 +78,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
[AppConnection.DNSMadeEasy]: AppConnectionPlanType.Regular,
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
[AppConnection.Railway]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,

View File

@@ -72,6 +72,8 @@ import { checklyConnectionService } from "./checkly/checkly-connection-service";
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
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 { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
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.GitLab]: ValidateGitLabConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
[AppConnection.DNSMadeEasy]: ValidateDNSMadeEasyConnectionCredentialsSchema,
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
@@ -875,6 +878,7 @@ export const appConnectionServiceFactory = ({
flyio: flyioConnectionService(connectAppConnectionById),
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
cloudflare: cloudflareConnectionService(connectAppConnectionById),
dnsMadeEasy: dnsMadeEasyConnectionService(connectAppConnectionById),
zabbix: zabbixConnectionService(connectAppConnectionById),
railway: railwayConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById),

View File

@@ -15,8 +15,8 @@ import {
TOracleDBConnectionInput,
TValidateOracleDBConnectionCredentialsSchema
} 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 { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@@ -106,6 +106,12 @@ import {
TDigitalOceanConnectionInput,
TValidateDigitalOceanCredentialsSchema
} from "./digital-ocean";
import {
TDNSMadeEasyConnection,
TDNSMadeEasyConnectionConfig,
TDNSMadeEasyConnectionInput,
TValidateDNSMadeEasyConnectionCredentialsSchema
} from "./dns-made-easy/dns-made-easy-connection-types";
import {
TFlyioConnection,
TFlyioConnectionConfig,
@@ -279,6 +285,7 @@ export type TAppConnection = { id: string } & (
| TGitLabConnection
| TCloudflareConnection
| TBitbucketConnection
| TDNSMadeEasyConnection
| TZabbixConnection
| TRailwayConnection
| TChecklyConnection
@@ -328,6 +335,7 @@ export type TAppConnectionInput = { id: string } & (
| TGitLabConnectionInput
| TCloudflareConnectionInput
| TBitbucketConnectionInput
| TDNSMadeEasyConnectionInput
| TZabbixConnectionInput
| TRailwayConnectionInput
| TChecklyConnectionInput
@@ -395,6 +403,7 @@ export type TAppConnectionConfig =
| TGitLabConnectionConfig
| TCloudflareConnectionConfig
| TBitbucketConnectionConfig
| TDNSMadeEasyConnectionConfig
| TZabbixConnectionConfig
| TRailwayConnectionConfig
| TChecklyConnectionConfig
@@ -439,6 +448,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateGitLabConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema
| TValidateBitbucketConnectionCredentialsSchema
| TValidateDNSMadeEasyConnectionCredentialsSchema
| TValidateZabbixConnectionCredentialsSchema
| TValidateRailwayConnectionCredentialsSchema
| TValidateChecklyConnectionCredentialsSchema

View File

@@ -0,0 +1,3 @@
export enum DNSMadeEasyConnectionMethod {
APIKeySecret = "api-key-secret"
}

View File

@@ -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;
};

View File

@@ -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] }));

View File

@@ -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
};
};

View File

@@ -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;
};

View File

@@ -663,7 +663,8 @@ export const authLoginServiceFactory = ({
timestamp: new Date().toISOString(),
ip: ipAddress,
userAgent,
siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com")
siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com"),
orgId: organizationId
},
template: SmtpTemplates.OrgAdminBreakglassAccess
});

View File

@@ -1,4 +1,5 @@
export enum AcmeDnsProvider {
Route53 = "route53",
Cloudflare = "cloudflare"
Cloudflare = "cloudflare",
DNSMadeEasy = "dns-made-easy"
}

View File

@@ -14,6 +14,7 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TAwsConnection } from "@app/services/app-connection/aws/aws-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 { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
@@ -43,6 +44,7 @@ import {
TUpdateAcmeCertificateAuthorityDTO
} from "./acme-certificate-authority-types";
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy";
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
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 (
{
caId,
@@ -241,8 +259,11 @@ export const orderCertificate = async (
throw new Error("Unsupported challenge type");
}
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
const { recordName, recordValue } = getAcmeChallengeRecord(
acmeCa.configuration.dnsProviderConfig.provider,
authz.identifier.value,
keyAuthorization
);
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
@@ -263,14 +284,26 @@ export const orderCertificate = async (
);
break;
}
case AcmeDnsProvider.DNSMadeEasy: {
await dnsMadeEasyInsertTxtRecord(
connection as TDNSMadeEasyConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
const { recordName, recordValue } = getAcmeChallengeRecord(
acmeCa.configuration.dnsProviderConfig.provider,
authz.identifier.value,
keyAuthorization
);
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
@@ -291,6 +324,15 @@ export const orderCertificate = async (
);
break;
}
case AcmeDnsProvider.DNSMadeEasy: {
await dnsMadeEasyDeleteTxtRecord(
connection as TDNSMadeEasyConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
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
await appConnectionService.validateAppConnectionUsageById(
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);
if (!ca) {

View File

@@ -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;
}
};

View File

@@ -1716,12 +1716,7 @@ export const internalCertificateAuthorityServiceFactory = ({
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const dn = parseDistinguishedName(csrObj.subject);
const cn = commonName || dn.commonName;
if (!cn)
throw new BadRequestError({
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
});
const cn = (commonName || dn.commonName) ?? "";
const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id,

View File

@@ -67,6 +67,12 @@ export const certificateEstV3ServiceFactory = ({
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);
if (!estConfig) {
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" });
}
if (!profile.caId) {
throw new BadRequestError({
message: "Self-signed certificates are not supported for EST enrollment"
});
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
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" });
}
if (!profile.caId) {
throw new BadRequestError({
message: "Self-signed certificates are not supported for EST enrollment"
});
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
throw new NotFoundError({ message: "EST configuration not found" });

View File

@@ -7,6 +7,7 @@ import { ormify, selectAllTableCols } from "@app/lib/knex";
import {
EnrollmentType,
IssuerType,
TCertificateProfile,
TCertificateProfileCertificate,
TCertificateProfileInsert,
@@ -198,6 +199,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
slug: result.slug,
description: result.description,
enrollmentType: result.enrollmentType as EnrollmentType,
issuerType: result.issuerType as IssuerType,
estConfigId: result.estConfigId,
apiConfigId: result.apiConfigId,
acmeConfigId: result.acmeConfigId,
@@ -239,12 +241,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
limit?: number;
search?: string;
enrollmentType?: EnrollmentType;
issuerType?: IssuerType;
caId?: string;
} = {},
tx?: Knex
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
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(
`${TableName.PkiCertificateProfile}.projectId`,
@@ -269,6 +272,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
}
if (issuerType) {
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.issuerType`, issuerType);
}
const query = baseQuery
.leftJoin(
TableName.PkiEstEnrollmentConfig,
@@ -338,8 +345,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
slug: result.slug,
description: result.description,
enrollmentType: result.enrollmentType as EnrollmentType,
issuerType: result.issuerType as IssuerType,
estConfigId: result.estConfigId,
apiConfigId: result.apiConfigId,
acmeConfigId: result.acmeConfigId,
createdAt: result.createdAt,
updatedAt: result.updatedAt,
estConfig,
@@ -359,12 +368,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
options: {
search?: string;
enrollmentType?: EnrollmentType;
issuerType?: IssuerType;
caId?: string;
} = {},
tx?: Knex
): Promise<number> => {
try {
const { search, enrollmentType, caId } = options;
const { search, enrollmentType, issuerType, caId } = options;
let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId });
@@ -384,6 +394,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
query = query.where({ caId });
}
if (issuerType) {
query = query.where({ issuerType });
}
const result = await query.count("*").first();
return parseInt((result as unknown as { count: string }).count || "0", 10);
} catch (error) {

View File

@@ -1,12 +1,13 @@
import RE2 from "re2";
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
.object({
projectId: z.string().uuid("Project ID must be valid"),
caId: z.string().uuid(),
caId: z.string().uuid().nullable().optional(),
certificateTemplateId: z.string().uuid(),
slug: z
.string()
@@ -15,6 +16,7 @@ export const createCertificateProfileSchema = z
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType),
issuerType: z.nativeEnum(IssuerType).default(IssuerType.CA),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
@@ -33,43 +35,100 @@ export const createCertificateProfileSchema = z
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (!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 !!data.estConfig;
}
return true;
},
{
message:
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration."
message: "EST enrollment type requires 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(),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
issuerType: z.nativeEnum(IssuerType).optional(),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
@@ -100,19 +160,34 @@ export const updateCertificateProfileSchema = z
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (data.apiConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.API) {
if (data.estConfig) {
return false;
}
return !data.apiConfig;
}
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),
search: z.string().optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
issuerType: z.nativeEnum(IssuerType).optional(),
caId: z.string().uuid().optional()
});
@@ -142,6 +218,6 @@ export const listCertificatesByProfileSchema = z.object({
profileId: z.string().uuid(),
offset: z.coerce.number().min(0).default(0),
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()
});

View File

@@ -22,7 +22,12 @@ import type { TKmsServiceFactory } from "../kms/kms-service";
import type { TProjectDALFactory } from "../project/project-dal";
import type { TCertificateProfileDALFactory } from "./certificate-profile-dal";
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", () => ({
crypto: {
@@ -90,6 +95,7 @@ describe("CertificateProfileService", () => {
description: "Test certificate profile",
slug: "test-profile",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfigId: "api-config-123",
@@ -272,6 +278,7 @@ describe("CertificateProfileService", () => {
slug: "new-profile",
description: "New test profile",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -312,6 +319,7 @@ describe("CertificateProfileService", () => {
slug: "new-profile",
description: "New test profile",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfigId: "api-config-123",
@@ -383,6 +391,7 @@ describe("CertificateProfileService", () => {
slug: "invalid-profile",
description: "Invalid test profile",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123"
};
@@ -401,6 +410,7 @@ describe("CertificateProfileService", () => {
slug: "api-profile",
description: "Profile with API enrollment",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -726,6 +736,7 @@ describe("CertificateProfileService", () => {
slug: "est-profile",
description: "Profile with EST enrollment",
enrollmentType: EnrollmentType.EST,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
estConfig: {
@@ -776,6 +787,7 @@ describe("CertificateProfileService", () => {
slug: "different-profile-name",
description: "Profile with duplicate slug",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -801,6 +813,7 @@ describe("CertificateProfileService", () => {
slug: "auto-renew-profile",
description: "Profile with auto-renewal",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -965,6 +978,7 @@ describe("CertificateProfileService", () => {
slug: "invalid-template-profile",
description: "Profile with invalid template",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "nonexistent-template",
apiConfig: {
@@ -990,6 +1004,7 @@ describe("CertificateProfileService", () => {
slug: "concurrent-profile",
description: "Profile created concurrently",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -1018,6 +1033,7 @@ describe("CertificateProfileService", () => {
slug: "cross-project-profile",
description: "Profile using template from different project",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-456",
apiConfig: {
@@ -1047,6 +1063,7 @@ describe("CertificateProfileService", () => {
slug: "invalid-slug-profile",
description: "Profile with invalid slug format",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {

View File

@@ -32,6 +32,7 @@ import { getProjectKmsCertificateKeyId } from "../project/project-fns";
import { TCertificateProfileDALFactory } from "./certificate-profile-dal";
import {
EnrollmentType,
IssuerType,
TCertificateProfile,
TCertificateProfileCertificate,
TCertificateProfileInsert,
@@ -39,6 +40,34 @@ import {
TCertificateProfileWithConfigs
} 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 (
projectId: string,
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey">,
@@ -163,7 +192,8 @@ export type TCertificateProfileServiceFactory = ReturnType<typeof certificatePro
const convertDalToService = (dalResult: Record<string, unknown>): TCertificateProfile => {
return {
...dalResult,
enrollmentType: dalResult.enrollmentType as EnrollmentType
enrollmentType: dalResult.enrollmentType as EnrollmentType,
issuerType: dalResult.issuerType as IssuerType
} as TCertificateProfile;
};
@@ -240,6 +270,8 @@ export const certificateProfileServiceFactory = ({
});
}
validateIssuerTypeConstraints(data.issuerType, data.enrollmentType, data.caId ?? null);
// Validate enrollment configuration requirements
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
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) => {
if (estConfig && existingProfile.estConfigId) {
@@ -569,6 +610,7 @@ export const certificateProfileServiceFactory = ({
limit = 20,
search,
enrollmentType,
issuerType,
caId
}: {
actor: ActorType;
@@ -580,6 +622,7 @@ export const certificateProfileServiceFactory = ({
limit?: number;
search?: string;
enrollmentType?: EnrollmentType;
issuerType?: IssuerType;
caId?: string;
}): Promise<{
profiles: TCertificateProfileWithConfigs[];
@@ -603,12 +646,14 @@ export const certificateProfileServiceFactory = ({
limit,
search,
enrollmentType,
issuerType,
caId
});
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
search,
enrollmentType,
issuerType,
caId
});

View File

@@ -10,16 +10,24 @@ export enum EnrollmentType {
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;
issuerType: IssuerType;
};
export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType"> & {
export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType" | "issuerType"> & {
enrollmentType: EnrollmentType;
issuerType: IssuerType;
};
export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType"> & {
export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType" | "issuerType"> & {
enrollmentType?: EnrollmentType;
issuerType?: IssuerType;
estConfig?: {
disableBootstrapCaValidation?: boolean;
passphrase?: string;

View File

@@ -22,7 +22,7 @@ import {
CertSubjectAttributeType
} from "@app/services/certificate-common/certificate-constants";
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 { ActorType, AuthMethod } from "../auth/auth-type";
@@ -40,18 +40,29 @@ vi.mock("../certificate-common/certificate-csr-utils", () => ({
describe("CertificateV3Service", () => {
let service: TCertificateV3ServiceFactory;
const mockCertificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction"> = {
const mockCertificateDAL: Pick<
TCertificateDALFactory,
"findOne" | "findById" | "updateById" | "transaction" | "create"
> = {
findOne: vi.fn(),
findById: 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>) => {
const mockTx = {};
return callback(mockTx);
})
};
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne"> = {
findOne: vi.fn()
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create"> = {
findOne: vi.fn(),
create: vi.fn()
};
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
@@ -150,7 +161,24 @@ describe("CertificateV3Service", () => {
},
pkiSyncQueue: {
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,
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -319,6 +348,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -508,6 +538,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -561,6 +592,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -721,6 +753,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -772,6 +805,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -933,6 +967,7 @@ describe("CertificateV3Service", () => {
id: profileId,
projectId: "project-123",
enrollmentType: EnrollmentType.EST, // Wrong enrollment type
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
createdAt: new Date(),
@@ -971,6 +1006,7 @@ describe("CertificateV3Service", () => {
caId: "ca-1",
certificateTemplateId: "template-1",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
createdAt: new Date(),
updatedAt: new Date(),
description: "Test profile for algorithm compatibility",
@@ -1552,6 +1588,7 @@ describe("CertificateV3Service", () => {
id: "profile-123",
projectId: "project-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
caId: "ca-123",
certificateTemplateId: "template-123",
apiConfig: {
@@ -1733,9 +1770,9 @@ describe("CertificateV3Service", () => {
});
});
it("should reject renewal if certificate is not from a profile", async () => {
const certWithoutProfile = { ...mockOriginalCert, profileId: null };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfile);
it("should reject renewal if certificate has no profile and no CA", async () => {
const certWithoutProfileAndCA = { ...mockOriginalCert, profileId: null, caId: null };
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfileAndCA);
// Set up transaction mock to properly handle errors
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
@@ -2008,6 +2045,7 @@ describe("CertificateV3Service", () => {
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
projectId: "project-123"
};
@@ -2084,6 +2122,7 @@ describe("CertificateV3Service", () => {
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
projectId: "project-123"
};
@@ -2129,6 +2168,7 @@ describe("CertificateV3Service", () => {
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
projectId: "project-123"
};
@@ -2172,6 +2212,7 @@ describe("CertificateV3Service", () => {
const mockProfile = {
id: "profile-123",
enrollmentType: EnrollmentType.API,
issuerType: IssuerType.CA,
projectId: "project-123"
};

View File

@@ -1,8 +1,9 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { randomUUID } from "crypto";
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 {
ProjectPermissionCertificateActions,
@@ -10,8 +11,11 @@ import {
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
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 { ms } from "@app/lib/ms";
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 { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
@@ -28,12 +32,25 @@ import {
TCertificateAuthorityWithAssociatedCa
} from "@app/services/certificate-authority/certificate-authority-dal";
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 { 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 { 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 {
extractAlgorithmsFromCSR,
extractCertificateRequestFromCSR
@@ -68,8 +85,9 @@ import {
} from "./certificate-v3-types";
type TCertificateV3ServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction" | "create">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById">;
@@ -85,6 +103,8 @@ type TCertificateV3ServiceFactoryDep = {
>;
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
projectDAL: TProjectDALFactory;
};
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 = (
profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } },
ttl: string,
@@ -348,8 +520,248 @@ const calculateFinalRenewBeforeDays = (
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 = ({
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
certificateAuthorityDAL,
certificateProfileDAL,
@@ -359,7 +771,9 @@ export const certificateV3ServiceFactory = ({
permissionService,
certificateSyncDAL,
pkiSyncDAL,
pkiSyncQueue
pkiSyncQueue,
kmsService,
projectDAL
}: TCertificateV3ServiceFactoryDep) => {
const issueCertificateFromProfile = async ({
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 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(
{ 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 } =
await internalCaService.issueCertFromCa({
caId: ca.id,
@@ -477,10 +946,11 @@ export const certificateV3ServiceFactory = ({
new Date(cert.notAfter)
);
await certificateDAL.updateById(cert.id, {
profileId,
renewBeforeDays: finalRenewBeforeDays
});
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
if (finalRenewBeforeDays !== undefined) {
updateData.renewBeforeDays = finalRenewBeforeDays;
}
await certificateDAL.updateById(cert.id, updateData);
let finalCertificateChain = bufferToString(certificateChain);
if (removeRootsFromChain) {
@@ -525,6 +995,12 @@ export const certificateV3ServiceFactory = ({
enrollmentType
);
if (!profile.caId) {
throw new BadRequestError({
message: "Self-signed certificates are not supported for CSR signing"
});
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
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));
await certificateDAL.updateById(cert.id, {
profileId,
renewBeforeDays: finalRenewBeforeDays
});
const updateData2: { profileId: string; renewBeforeDays?: number } = { profileId };
if (finalRenewBeforeDays !== undefined) {
updateData2.renewBeforeDays = finalRenewBeforeDays;
}
await certificateDAL.updateById(cert.id, updateData2);
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
@@ -640,10 +1117,25 @@ export const certificateV3ServiceFactory = ({
commonName: certificateOrder.commonName,
keyUsages: certificateOrder.keyUsages,
extendedKeyUsages: certificateOrder.extendedKeyUsages,
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
type: san.type === "dns" ? CertSubjectAlternativeNameType.DNS_NAME : CertSubjectAlternativeNameType.IP_ADDRESS,
value: san.value
})),
subjectAlternativeNames: certificateOrder.altNames.map((san) => {
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
};
}),
validity: certificateOrder.validity,
notBefore: certificateOrder.notBefore,
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);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
@@ -741,15 +1239,19 @@ export const certificateV3ServiceFactory = ({
});
}
const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
let profile = null;
if (originalCert.profileId) {
profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== EnrollmentType.API) {
throw new ForbiddenRequestError({
message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint"
});
if (profile.enrollmentType !== EnrollmentType.API) {
throw new ForbiddenRequestError({
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);
@@ -761,10 +1263,11 @@ export const certificateV3ServiceFactory = ({
}
if (!internal) {
const projectId = profile?.projectId || originalCert.projectId;
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: profile.projectId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
@@ -776,33 +1279,46 @@ export const certificateV3ServiceFactory = ({
);
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
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) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
const eligibilityCheck = validateRenewalEligibility(originalCert, ca);
if (!eligibilityCheck.isEligible) {
await certificateDAL.updateById(originalCert.id, {
renewalError: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
});
throw new BadRequestError({
message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
});
}
validateCaSupport(ca, "direct certificate issuance");
}
const eligibilityCheck = validateRenewalEligibility(originalCert, ca);
if (!eligibilityCheck.isEligible) {
await certificateDAL.updateById(originalCert.id, {
renewalError: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
});
throw new BadRequestError({
message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}`
});
}
const templateId = profile?.certificateTemplateId || originalCert.certificateTemplateId;
const template = templateId
? await certificateTemplateV2Service.getTemplateV2ById({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId,
internal
})
: null;
validateCaSupport(ca, "direct certificate issuance");
const template = await certificateTemplateV2Service.getTemplateV2ById({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId: profile.certificateTemplateId,
internal
});
if (!template) {
if (!template && profile) {
throw new NotFoundError({ message: "Certificate template not found for this profile" });
}
@@ -857,10 +1373,13 @@ export const certificateV3ServiceFactory = ({
keyAlgorithm: originalCert.keyAlgorithm || undefined
};
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
certificateRequest
);
let validationResult: { isValid: boolean; errors: string[] } = { isValid: true, errors: [] };
if (profile?.certificateTemplateId) {
validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
certificateRequest
);
}
if (!validationResult.isValid) {
await certificateDAL.updateById(originalCert.id, {
@@ -872,14 +1391,28 @@ export const certificateV3ServiceFactory = ({
});
}
validateAlgorithmCompatibility(ca, template);
const notBefore = new Date();
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 } =
await internalCaService.issueCertFromCa({
let certificate: string;
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,
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate",
commonName: originalCert.commonName || "",
@@ -900,20 +1433,72 @@ export const certificateV3ServiceFactory = ({
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) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
}
await certificateDAL.updateById(
newCert.id,
{
profileId: originalCert.profileId,
renewBeforeDays: finalRenewBeforeDays,
// For self-signed certificates, we already set the renewal data during creation
// For CA-signed certificates, we need to set it now
if (issuerType === IssuerType.CA) {
const renewalUpdateData: {
profileId: string | null;
renewedFromCertificateId: string;
renewBeforeDays?: number;
} = {
profileId: originalCert.profileId || null,
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(
originalCert.id,
@@ -953,8 +1538,8 @@ export const certificateV3ServiceFactory = ({
certificateChain: finalCertificateChain,
serialNumber: renewalResult.serialNumber,
certificateId: renewalResult.newCert.id,
projectId: renewalResult.profile.projectId,
profileName: renewalResult.profile.slug,
projectId: renewalResult.originalCert.projectId,
profileName: renewalResult.profile?.slug || "Self-signed Certificate",
commonName: renewalResult.originalCert.commonName || ""
};
};

View File

@@ -309,6 +309,14 @@ export const certificateServiceFactory = ({
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({
projectId: cert.projectId,
projectDAL,
@@ -599,6 +607,14 @@ export const certificateServiceFactory = ({
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({
projectId: cert.projectId,
projectDAL,

View File

@@ -270,7 +270,13 @@ export const identityKubernetesAuthServiceFactory = ({
}
)
.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) {
logger.error(
{ response: err.response, host, port, tokenReviewerJwtSnippet, serviceAccountJwtSnippet },
"tokenReviewCallbackRaw: Kubernetes token review request error (request error)"
);
if (err.response) {
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;
});

View File

@@ -105,7 +105,9 @@ export enum IntegrationUrls {
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
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 () => {

View File

@@ -94,6 +94,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"),
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("role").withSchema(TableName.MembershipRole).as("membershipRole"),
db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"),
@@ -180,6 +181,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
label: "roles" as const,
mapper: ({
roleSlug,
roleName,
membershipRoleId,
membershipRole,
membershipRoleIsTemporary,
@@ -193,6 +195,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
id: membershipRoleId,
role: membershipRole,
customRoleSlug: roleSlug,
customRoleName: roleName,
temporaryRange: membershipRoleTemporaryRange,
temporaryMode: membershipRoleTemporaryMode,
temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime,

View File

@@ -129,7 +129,7 @@ export const newOrgMembershipUserFactory = ({
recipients: emails as string[],
substitutions: {
subOrganizationName: orgDetails.slug,
callback_url: `${appCfg.SITE_URL}/organization/projects?subOrganization=${orgDetails.slug}`
callback_url: `${appCfg.SITE_URL}/organizations/${dto.permission.orgId}/projects?subOrganization=${orgDetails.slug}`
}
});
} else {

View File

@@ -357,7 +357,7 @@ export const isBotInstalledInTenant = async (
}
};
export const buildTeamsPayload = (notification: TNotification) => {
export const buildTeamsPayload = (orgId: string, notification: TNotification) => {
const appCfg = getConfig();
switch (notification.type) {
@@ -402,7 +402,7 @@ export const buildTeamsPayload = (notification: TNotification) => {
{
type: "Action.OpenUrl",
title: "View request in Infisical",
url: `${appCfg.SITE_URL}/projects/secret-management/${payload.projectId}/approval?requestId=${payload.requestId}`
url: `${appCfg.SITE_URL}/organizations/${orgId}/projects/secret-management/${payload.projectId}/approval?requestId=${payload.requestId}`
}
]
};
@@ -590,10 +590,11 @@ export class TeamsBot extends TeamsActivityHandler {
tenantId: string,
channelId: string,
teamId: string,
orgId: string,
notification: TNotification
) {
try {
const { adaptiveCard } = buildTeamsPayload(notification);
const { adaptiveCard } = buildTeamsPayload(orgId, notification);
const adaptiveCardActivity = {
type: "message",

View File

@@ -759,7 +759,7 @@ export const microsoftTeamsServiceFactory = ({
});
for await (const channelId of target.channelIds) {
await teamsBot.sendMessageToChannel(botAccessToken, tenantId, channelId, target.teamId, notification);
await teamsBot.sendMessageToChannel(botAccessToken, tenantId, channelId, target.teamId, orgId, notification);
}
};

View File

@@ -1984,7 +1984,7 @@ export const projectServiceFactory = ({
projectTypeUrl = "cert-management";
}
const callbackPath = `/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`;
const callbackPath = `/organizations/${project.orgId}/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`;
await notificationService.createUserNotifications(
projectMembers

View File

@@ -391,7 +391,7 @@ export const secretSharingServiceFactory = ({
substitutions: {
name: secretRequest.name,
respondentUsername,
secretRequestUrl: `${appCfg.SITE_URL}/organization/secret-sharing?selectedTab=request-secret`
secretRequestUrl: `${appCfg.SITE_URL}/organizations/${secretRequest.orgId}/secret-sharing?selectedTab=request-secret`
},
template: SmtpTemplates.SecretRequestCompleted
});

View File

@@ -932,7 +932,7 @@ export const secretSyncQueueFactory = ({
break;
}
const baseProjectPath = `/projects/secret-management/${projectId}`;
const baseProjectPath = `/organizations/${project.orgId}/projects/secret-management/${projectId}`;
const overviewPath = `${baseProjectPath}/overview`;
const syncPath = `${baseProjectPath}/integrations/secret-syncs/${destination}/${secretSync.id}`;

View File

@@ -742,7 +742,7 @@ export const secretQueueFactory = ({
environment: jobPayload.environmentName,
count: jobPayload.count,
projectName: project.name,
integrationUrl: `${appCfg.SITE_URL}/projects/secret-management/${project.id}/integrations?selectedTab=native-integrations`
integrationUrl: `${appCfg.SITE_URL}/organizations/${project.orgId}/projects/secret-management/${project.id}/integrations?selectedTab=native-integrations`
}
});
}

View File

@@ -30,28 +30,35 @@ export const serviceTokenDALFactory = (db: TDbClient) => {
const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0) => {
try {
const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] =
await (tx || db.replicaNode())(TableName.ServiceToken)
.leftJoin<TUsers>(
TableName.Users,
`${TableName.Users}.id`,
db.raw(`${TableName.ServiceToken}."createdBy"::uuid`)
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`)
.whereRaw(
`${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false`
)
.whereNotNull(`${TableName.Users}.email`)
.select(
db.ref("id").withSchema(TableName.ServiceToken),
db.ref("name").withSchema(TableName.ServiceToken),
db.ref("projectId").withSchema(TableName.ServiceToken),
db.ref("createdBy").withSchema(TableName.ServiceToken),
db.ref("email").withSchema(TableName.Users).as("createdByEmail"),
db.ref("name").withSchema(TableName.Project).as("projectName")
)
.limit(batchSize)
.offset(offset);
const batch: {
name: string;
projectName: string;
createdByEmail: string;
id: string;
projectId: string;
orgId: string;
}[] = await (tx || db.replicaNode())(TableName.ServiceToken)
.leftJoin<TUsers>(
TableName.Users,
`${TableName.Users}.id`,
db.raw(`${TableName.ServiceToken}."createdBy"::uuid`)
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`)
.whereRaw(
`${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false`
)
.whereNotNull(`${TableName.Users}.email`)
.select(
db.ref("id").withSchema(TableName.ServiceToken),
db.ref("name").withSchema(TableName.ServiceToken),
db.ref("projectId").withSchema(TableName.ServiceToken),
db.ref("createdBy").withSchema(TableName.ServiceToken),
db.ref("email").withSchema(TableName.Users).as("createdByEmail"),
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("orgId").withSchema(TableName.Project).as("orgId")
)
.limit(batchSize)
.offset(offset);
return batch;
} catch (err) {

View File

@@ -225,7 +225,7 @@ export const serviceTokenServiceFactory = ({
substitutions: {
tokenName: token.name,
projectName: token.projectName,
url: `${appCfg.SITE_URL}/projects/secret-management/${token.projectId}/access-management?selectedTab=service-tokens`
url: `${appCfg.SITE_URL}/organizations/${token.orgId}/projects/secret-management/${token.projectId}/access-management?selectedTab=service-tokens`
}
});
await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true });

View File

@@ -7,6 +7,7 @@ import { BaseLink } from "./BaseLink";
interface OrgAdminBreakglassAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
email: string;
timestamp: string;
orgId: string;
ip: string;
userAgent: string;
}
@@ -15,6 +16,7 @@ export const OrgAdminBreakglassAccessTemplate = ({
email,
siteUrl,
timestamp,
orgId,
ip,
userAgent
}: OrgAdminBreakglassAccessTemplateProps) => {
@@ -36,7 +38,7 @@ export const OrgAdminBreakglassAccessTemplate = ({
<Text className="text-[14px] mt-[4px]">{userAgent}</Text>
<Text className="text-[14px]">
If you'd like to disable Admin SSO Bypass, please visit{" "}
<BaseLink href={`${siteUrl}/organization/settings`}>Organization Security Settings</BaseLink>.
<BaseLink href={`${siteUrl}/organizations/${orgId}/settings`}>Organization Security Settings</BaseLink>.
</Text>
</Section>
</BaseEmailWrapper>
@@ -51,5 +53,6 @@ OrgAdminBreakglassAccessTemplate.PreviewProps = {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15",
timestamp: "Tue Apr 29 2025 23:03:27 GMT+0000 (Coordinated Universal Time)",
siteUrl: "https://infisical.com",
email: "august@infisical.com"
email: "august@infisical.com",
orgId: "123"
} as OrgAdminBreakglassAccessTemplateProps;

View File

@@ -118,6 +118,7 @@
"integrations/app-connections/cloudflare",
"integrations/app-connections/databricks",
"integrations/app-connections/digital-ocean",
"integrations/app-connections/dns-made-easy",
"integrations/app-connections/flyio",
"integrations/app-connections/gcp",
"integrations/app-connections/github",
@@ -756,7 +757,7 @@
{
"group": "Infrastructure Integrations",
"pages": [
"documentation/platform/pki/pki-issuer",
"documentation/platform/pki/k8s-cert-manager",
"documentation/platform/pki/integration-guides/gloo-mesh",
"documentation/platform/pki/integration-guides/windows-server-acme",
"documentation/platform/pki/integration-guides/nginx-certbot",

View File

@@ -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.
- [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.

View File

@@ -17,7 +17,7 @@ their **ACME Directory URL** such as:
- ZeroSSL: `https://acme.zerossl.com/v2/DV90`.
- 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.
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.
</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>
</Step>
<Step title="Register an ACME-compatible CA">

View File

@@ -19,10 +19,12 @@ where you can manage various aspects of its lifecycle including deployment to cl
## 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.
Refer to the documentation for each enrollment method to learn more about how to issue certificates using 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.
- 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

View File

@@ -21,7 +21,8 @@ Here's some guidance on each field:
- Name: A slug-friendly name for the profile such as `web-servers`.
- 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.
- Enrollment Method: The enrollment method that should be used to enroll certificates for the profile such as ACME, EST, API, etc.

View File

@@ -5,7 +5,7 @@ sidebarTitle: "ACME"
## 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.
Infisical's ACME enrollment method is based on [RFC 8555](https://datatracker.ietf.org/doc/html/rfc8555/).

View File

@@ -1,9 +1,9 @@
---
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.
@@ -182,4 +182,5 @@ Before you begin, make sure you have:
</Note>
</Step>
</Steps>
</Steps>

View File

@@ -1,13 +1,13 @@
---
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
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/).
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
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.
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:
- 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
- Multiple workload clusters use the same Infisical PKI root to enable cross-cluster mTLS communication
- Certificate profiles in Infisical are configured for intermediate CA usage with appropriate key usage and constraints
- Multiple workload clusters use the same Infisical root to enable cross-cluster mTLS communication
## 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.
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.

View File

@@ -1,9 +1,9 @@
---
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.
@@ -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.
</Note>
</Step>
</Steps>
</Steps>

View File

@@ -1,9 +1,9 @@
---
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).

View File

@@ -1,9 +1,9 @@
---
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.
@@ -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.
</Note>
</Step>
</Steps>
</Steps>

View File

@@ -1,9 +1,9 @@
---
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.
@@ -191,4 +191,5 @@ Before you begin, make sure you have:
</Tab>
</Tabs>
</Step>
</Steps>

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

After

Width:  |  Height:  |  Size: 368 KiB

View File

@@ -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.
![Navigate to Account Information](/images/app-connections/dns-made-easy/nav-to-account-info.png)
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.
![Generate API Credentials](/images/app-connections/dns-made-easy/generate-new-api-credentials.png)
</Step>
<Step title="Copy Your API Key and Secret Key">
After creation, copy your API key and secret key.
![Generated API Token](/images/app-connections/dns-made-easy/copy-api-credentials.png)
<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. ![App
Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Add Connection">
Select the **DNS Made Easy Connection** option from the connection options
modal. ![Select DNS Made Easy
Connection](/images/app-connections/dns-made-easy/dns-made-easy-app-connection-select.png)
</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. ![Connect to
DNS Made
Easy](/images/app-connections/dns-made-easy/dns-made-easy-app-connection-form.png)
</Step>
<Step title="Connection Created">
Your **DNS Made Easy Connection** is now available for use in your Infisical
projects. ![DNS Made Easy Connection
Created](/images/app-connections/dns-made-easy/dns-made-easy-app-connection-created.png)
</Step>
</Steps>

View File

@@ -50,6 +50,11 @@ Prerequisites:
![integrations octopus deploy add
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.
![integrations octopus deploy add service account to team](/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png)

View File

@@ -1,70 +1,388 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo } from "react";
export const AppConnectionsBrowser = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All');
const [searchTerm, setSearchTerm] = useState("");
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 = [
{"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": "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"},
{"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"},
{"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"},
{"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": "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) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
{
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: "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",
},
{
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",
},
{
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",
},
{
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) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
const filteredConnections = useMemo(() => {
let filtered = connections;
if (selectedCategory !== 'All') {
filtered = filtered.filter(connection => connection.category === selectedCategory);
if (selectedCategory !== "All") {
filtered = filtered.filter(
(connection) => connection.category === selectedCategory
);
}
if (searchTerm) {
filtered = filtered.filter(connection =>
connection.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
connection.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
connection.category.toLowerCase().includes(searchTerm.toLowerCase())
filtered = filtered.filter(
(connection) =>
connection.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
connection.description
.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
connection.category.toLowerCase().includes(searchTerm.toLowerCase())
);
}
@@ -77,8 +395,18 @@ export const AppConnectionsBrowser = () => {
<div className="mb-6">
<div className="relative w-full">
<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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
<svg
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>
</div>
<input
@@ -94,14 +422,14 @@ export const AppConnectionsBrowser = () => {
{/* Category Filter */}
<div className="mb-6">
<div className="flex flex-wrap gap-2">
{categories.map(category => (
{categories.map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors shadow-sm ${
selectedCategory === category
? '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-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"
}`}
>
{category}
@@ -113,8 +441,9 @@ export const AppConnectionsBrowser = () => {
{/* Results Count */}
<div className="mb-4">
<p className="text-sm text-gray-600">
{filteredConnections.length} app connection{filteredConnections.length !== 1 ? 's' : ''} found
{selectedCategory !== 'All' && ` in ${selectedCategory}`}
{filteredConnections.length} app connection
{filteredConnections.length !== 1 ? "s" : ""} found
{selectedCategory !== "All" && ` in ${selectedCategory}`}
{searchTerm && ` for "${searchTerm}"`}
</p>
</div>
@@ -147,13 +476,17 @@ export const AppConnectionsBrowser = () => {
) : (
<div className="text-center py-8">
<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 && (
<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>
);
};
};

View File

@@ -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

View File

@@ -289,7 +289,7 @@
}
},
"project": {
"title": "Settings",
"title": "Project Settings",
"description": "These settings only apply to the currently selected Project.",
"danger-zone": "Danger Zone",
"delete-project": "Delete Project",

View File

@@ -20,9 +20,14 @@ export default function TeamInviteStep(): JSX.Element {
const { mutateAsync } = useAddUsersToOrg();
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
const orgId = String(localStorage.getItem("orgData.id"));
// Redirect user to the getting started page
const redirectToHome = async () => {
navigate({ to: "/organization/projects" as const });
navigate({
to: orgId ? ("/organizations/$orgId/projects" as const) : "/",
params: { orgId }
});
};
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
@@ -32,7 +37,7 @@ export default function TeamInviteStep(): JSX.Element {
.map(async (email) => {
mutateAsync({
inviteeEmails: [email],
organizationId: String(localStorage.getItem("orgData.id")),
organizationId: orgId,
organizationRoleSlug: "member"
});
});

View File

@@ -70,7 +70,8 @@ export default function NavHeader({
{currentOrg?.name?.charAt(0)}
</div>
<Link
to="/organization/projects"
to="/organizations/$orgId/projects"
params={{ orgId: currentOrg.id }}
className="truncate pl-0.5 text-sm font-medium text-primary/80 hover:text-primary"
>
{currentOrg?.name}
@@ -90,8 +91,8 @@ export default function NavHeader({
<FontAwesomeIcon icon={faAngleRight} className="mr-3 ml-3 text-sm text-gray-400" />
{pageName === "Secrets" ? (
<Link
to="/projects/secret-management/$projectId/overview"
params={{ projectId: currentProject.id }}
to="/organizations/$orgId/projects/secret-management/$projectId/overview"
params={{ orgId: currentOrg?.id || "", projectId: currentProject.id }}
className="text-sm font-medium text-primary/80 hover:text-primary"
>
{pageName}
@@ -126,8 +127,8 @@ export default function NavHeader({
<div className="flex items-center space-x-3">
<FontAwesomeIcon icon={faAngleRight} className="mr-1.5 ml-3 text-xs text-gray-400" />
<Link
to="/projects/secret-management/$projectId/secrets/$envSlug"
params={{ projectId: currentProject.id, envSlug: routerEnvSlug }}
to="/organizations/$orgId/projects/secret-management/$projectId/secrets/$envSlug"
params={{ orgId: currentOrg.id, projectId: currentProject.id, envSlug: routerEnvSlug }}
className="text-sm font-medium text-primary/80 hover:text-primary"
>
{userAvailableEnvs?.find(({ slug }) => slug === currentEnv)?.name}
@@ -188,8 +189,9 @@ export default function NavHeader({
</div>
) : (
<Link
to="/projects/secret-management/$projectId/secrets/$envSlug"
to="/organizations/$orgId/projects/secret-management/$projectId/secrets/$envSlug"
params={{
orgId: currentOrg?.id || "",
projectId: currentProject.id,
envSlug: routerEnvSlug || ""
}}

View File

@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { useOrganization } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { createNotification } from "../notifications";
@@ -23,6 +24,7 @@ export const SecretDashboardPathBreadcrumb = ({
projectId,
disableCopy
}: Props) => {
const { currentOrg } = useOrganization();
const [, isCopying, setIsCopying] = useTimedReset({
initialState: false
});
@@ -69,8 +71,9 @@ export const SecretDashboardPathBreadcrumb = ({
</div>
) : (
<Link
to="/projects/secret-management/$projectId/secrets/$envSlug"
to="/organizations/$orgId/projects/secret-management/$projectId/secrets/$envSlug"
params={{
orgId: currentOrg.id,
projectId,
envSlug: environmentSlug
}}

View File

@@ -57,7 +57,8 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
});
navigate({
to: "/organization/projects"
to: "/organizations/$orgId/projects",
params: { orgId: organization.id }
});
localStorage.setItem("orgData.id", organization.id);

View File

@@ -87,17 +87,24 @@ const shouldShowConditionalAccess = (
folderPath: string,
conditionalFields: string[]
): boolean => {
return actionRuleMap.some((rule) => {
// Find all rules that apply to this environment/path
const applicableRules = actionRuleMap.filter((rule) => {
const ruleConditions = rule[action]?.conditions;
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);
});
// 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 = (

View File

@@ -157,7 +157,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
onOpenChange(false);
navigate({
to: getProjectHomePage(project.type, project.environments),
params: { projectId: project.id }
params: { projectId: project.id, orgId: currentOrg.id }
});
};
const onSubmit = handleSubmit((data) => {

View File

@@ -3,14 +3,7 @@ import { ReactNode } from "@tanstack/react-router";
import { LucideIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import {
Badge,
InstanceIcon,
OrgIcon,
ProjectIcon,
SubOrgIcon,
TBadgeProps
} from "@app/components/v3";
import { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon } from "@app/components/v3";
import { ProjectType } from "@app/hooks/api/projects/types";
type Props = {
@@ -21,41 +14,40 @@ type Props = {
scope: "org" | "namespace" | "instance" | ProjectType | null;
};
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: LucideIcon }> = {
org: { label: "Organization", icon: OrgIcon },
[ProjectType.SecretManager]: { label: "Project", icon: ProjectIcon },
[ProjectType.CertificateManager]: { label: "Project", icon: ProjectIcon },
[ProjectType.SSH]: { label: "Project", icon: ProjectIcon },
[ProjectType.KMS]: { label: "Project", icon: ProjectIcon },
[ProjectType.PAM]: { label: "Project", icon: ProjectIcon },
[ProjectType.SecretScanning]: { label: "Project", icon: ProjectIcon },
namespace: { label: "Sub-Organization", icon: SubOrgIcon },
instance: { label: "Server", 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"
const SCOPE_BADGE: Record<NonNullable<Props["scope"]>, { icon: LucideIcon; className: string }> = {
org: { className: "text-org", icon: OrgIcon },
[ProjectType.SecretManager]: { className: "text-project", icon: ProjectIcon },
[ProjectType.CertificateManager]: { className: "text-project", icon: ProjectIcon },
[ProjectType.SSH]: { className: "text-project", icon: ProjectIcon },
[ProjectType.KMS]: { className: "text-project", icon: ProjectIcon },
[ProjectType.PAM]: { className: "text-project", icon: ProjectIcon },
[ProjectType.SecretScanning]: { className: "text-project", icon: ProjectIcon },
namespace: { className: "text-sub-org", icon: SubOrgIcon },
instance: { className: "text-neutral", icon: InstanceIcon }
};
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
<div className={twMerge("mb-10 w-full", className)}>
<div className="flex w-full justify-between">
<div className="mr-4 flex w-full items-center">
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
{scope && (
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5">
{createElement(SCOPE_NAME[scope].icon)}
{SCOPE_NAME[scope].label}
</Badge>
)}
<h1
className={twMerge(
"text-3xl font-medium text-white capitalize underline decoration-2 underline-offset-4",
scope === "org" && "decoration-org/90",
scope === "instance" && "decoration-neutral/90",
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 className="flex items-center gap-2">{children}</div>
</div>

View File

@@ -47,8 +47,8 @@ export const Tab = ({
}) => (
<TabsPrimitive.Trigger
className={twMerge(
"flex h-10 cursor-pointer items-center justify-center border-transparent",
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-400 transition-all select-none",
"flex h-11 cursor-pointer items-center justify-center border-transparent",
"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",
"border-b hover:text-mineshaft-200",
"data-[state=active]:border-mineshaft-400 data-[state=active]:text-white",

View File

@@ -1,4 +1,4 @@
import { BookOpenIcon } from "lucide-react";
import { ExternalLinkIcon } from "lucide-react";
import { Badge } from "@app/components/v3";
@@ -8,10 +8,10 @@ type TDocumentationLinkBadgeProps = {
export function DocumentationLinkBadge({ href }: TDocumentationLinkBadgeProps) {
return (
<Badge variant="neutral" asChild>
<Badge variant="info" asChild>
<a href={href} target="_blank" rel="noopener noreferrer">
<BookOpenIcon />
Documentation
<ExternalLinkIcon />
</a>
</Badge>
);

View File

@@ -25,343 +25,343 @@ export const ROUTE_PATHS = Object.freeze({
Organization: {
Settings: {
OauthCallbackPage: setRoute(
"/organization/settings/oauth/callback",
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
"/organizations/$orgId/settings/oauth/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/settings/oauth/callback"
)
},
SecretSharing: setRoute(
"/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
"/organizations/$orgId/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/secret-sharing/"
),
SettingsPage: setRoute(
"/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
"/organizations/$orgId/settings",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/settings/"
),
GroupDetailsByIDPage: setRoute(
"/organization/groups/$groupId",
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId"
"/organizations/$orgId/groups/$groupId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/groups/$groupId"
),
IdentityDetailsByIDPage: setRoute(
"/organization/identities/$identityId",
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId"
"/organizations/$orgId/identities/$identityId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/identities/$identityId"
),
UserDetailsByIDPage: setRoute(
"/organization/members/$membershipId",
"/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId"
"/organizations/$orgId/members/$membershipId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/members/$membershipId"
),
AccessControlPage: setRoute(
"/organization/access-management",
"/_authenticate/_inject-org-details/_org-layout/organization/access-management"
"/organizations/$orgId/access-management",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/access-management"
),
RoleByIDPage: setRoute(
"/organization/roles/$roleId",
"/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId"
"/organizations/$orgId/roles/$roleId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/roles/$roleId"
),
AppConnections: {
OauthCallbackPage: setRoute(
"/organization/app-connections/$appConnection/oauth/callback",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
"/organizations/$orgId/app-connections/$appConnection/oauth/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/app-connections/$appConnection/oauth/callback"
)
},
NetworkingPage: setRoute(
"/organization/networking",
"/_authenticate/_inject-org-details/_org-layout/organization/networking"
"/organizations/$orgId/networking",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/networking"
)
},
SecretManager: {
ApprovalPage: setRoute(
"/projects/secret-management/$projectId/approval",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval"
"/organizations/$orgId/projects/secret-management/$projectId/approval",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/approval"
),
SecretDashboardPage: setRoute(
"/projects/secret-management/$projectId/secrets/$envSlug",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug"
"/organizations/$orgId/projects/secret-management/$projectId/secrets/$envSlug",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug"
),
RollbackPreviewPage: setRoute(
"/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
"/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
),
CommitDetailsPage: setRoute(
"/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId"
"/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId"
),
CommitsPage: setRoute(
"/projects/secret-management/$projectId/commits/$environment/$folderId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId"
"/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId"
),
OverviewPage: setRoute(
"/projects/secret-management/$projectId/overview",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview"
"/organizations/$orgId/projects/secret-management/$projectId/overview",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/overview"
),
IntegrationsListPage: setRoute(
"/projects/secret-management/$projectId/integrations",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/"
"/organizations/$orgId/projects/secret-management/$projectId/integrations",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/"
),
IntegrationDetailsByIDPage: setRoute(
"/projects/secret-management/$projectId/integrations/$integrationId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/$integrationId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId"
),
SecretSyncDetailsByIDPage: setRoute(
"/projects/secret-management/$projectId/integrations/secret-syncs/$destination/$syncId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/secret-syncs/$destination/$syncId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId"
),
Integratons: {
SelectIntegrationAuth: setRoute(
"/projects/secret-management/$projectId/integrations/select-integration-auth",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/select-integration-auth",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth"
),
HerokuOauthCallbackPage: setRoute(
"/projects/secret-management/$projectId/integrations/heroku/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/heroku/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback"
),
HerokuConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/heroku/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/heroku/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/create"
),
AwsParameterStoreConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/aws-parameter-store/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/aws-parameter-store/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create"
),
AwsSecretManagerConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/aws-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/aws-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create"
),
AzureAppConfigurationsOauthCallbackPage: setRoute(
"/projects/secret-management/$projectId/integrations/azure-app-configuration/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-app-configuration/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback"
),
AzureAppConfigurationsConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/azure-app-configuration/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-app-configuration/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create"
),
AzureDevopsConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/azure-devops/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-devops/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-devops/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-devops/create"
),
AzureKeyVaultAuthorizePage: setRoute(
"/projects/secret-management/$projectId/integrations/azure-key-vault/authorize",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-key-vault/authorize",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize"
),
AzureKeyVaultOauthCallbackPage: setRoute(
"/projects/secret-management/$projectId/integrations/azure-key-vault/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-key-vault/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback"
),
AzureKeyVaultConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/azure-key-vault/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-key-vault/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/create"
),
BitbucketOauthCallbackPage: setRoute(
"/projects/secret-management/$projectId/integrations/bitbucket/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/bitbucket/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback"
),
BitbucketConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/bitbucket/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/bitbucket/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/create"
),
ChecklyConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/checkly/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/checkly/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/checkly/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/checkly/create"
),
CircleConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/circleci/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/circleci/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/circleci/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/circleci/create"
),
CloudflarePagesConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/cloudflare-pages/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/cloudflare-pages/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create"
),
DigitalOceanAppPlatformConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create"
),
CloudflareWorkersConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/cloudflare-workers/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/cloudflare-workers/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create"
),
CodefreshConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/codefresh/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/codefresh/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/codefresh/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/codefresh/create"
),
GcpSecretManagerConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/gcp-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/gcp-secret-manager/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create"
),
GcpSecretManagerOauthCallbackPage: setRoute(
"/projects/secret-management/$projectId/integrations/gcp-secret-manager/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/gcp-secret-manager/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback"
),
GithubConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/github/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/github/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/create"
),
GithubOauthCallbackPage: setRoute(
"/projects/secret-management/$projectId/integrations/github/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/github/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/oauth2/callback"
),
GitlabConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/gitlab/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/gitlab/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/create"
),
GitlabOauthCallbackPage: setRoute(
"/projects/secret-management/$projectId/integrations/gitlab/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/gitlab/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback"
),
VercelOauthCallbackPage: setRoute(
"/projects/secret-management/$projectId/integrations/vercel/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/vercel/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback"
),
VercelConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/vercel/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/vercel/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/create"
),
FlyioConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/flyio/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/flyio/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/flyio/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/flyio/create"
),
HashicorpVaultConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/hashicorp-vault/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/hashicorp-vault/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create"
),
HasuraCloudConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/hasura-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/hasura-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/hasura-cloud/create"
),
LaravelForgeConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/laravel-forge/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/laravel-forge/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/laravel-forge/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/laravel-forge/create"
),
NorthflankConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/northflank/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/northflank/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/northflank/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/northflank/create"
),
RailwayConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/railway/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/railway/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/railway/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/railway/create"
),
RenderConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/render/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/render/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/render/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/render/create"
),
RundeckConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/rundeck/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/rundeck/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/rundeck/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/rundeck/create"
),
WindmillConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/windmill/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/windmill/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/windmill/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/windmill/create"
),
TravisCIConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/travisci/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/travisci/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/travisci/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/travisci/create"
),
TerraformCloudConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/terraform-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/terraform-cloud/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/terraform-cloud/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/terraform-cloud/create"
),
TeamcityConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/teamcity/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/teamcity/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/teamcity/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/teamcity/create"
),
SupabaseConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/supabase/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/supabase/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/supabase/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/supabase/create"
),
OctopusDeployCloudConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/octopus-deploy/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/octopus-deploy/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/octopus-deploy/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/octopus-deploy/create"
),
DatabricksConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/databricks/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/databricks/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/databricks/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/databricks/create"
),
QoveryConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/qovery/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/qovery/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/qovery/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/qovery/create"
),
Cloud66ConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/cloud-66/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloud-66/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/cloud-66/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloud-66/create"
),
NetlifyConfigurePage: setRoute(
"/projects/secret-management/$projectId/integrations/netlify/create",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/create"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/netlify/create",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/create"
),
NetlifyOuathCallbackPage: setRoute(
"/projects/secret-management/$projectId/integrations/netlify/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback"
"/organizations/$orgId/projects/secret-management/$projectId/integrations/netlify/oauth2/callback",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback"
)
}
},
CertManager: {
CertAuthDetailsByIDPage: setRoute(
"/projects/cert-management/$projectId/ca/$caName",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName"
"/organizations/$orgId/projects/cert-management/$projectId/ca/$caName",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName"
),
SubscribersPage: setRoute(
"/projects/cert-management/$projectId/subscribers",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
"/organizations/$orgId/projects/cert-management/$projectId/subscribers",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
),
CertificateAuthoritiesPage: setRoute(
"/projects/cert-management/$projectId/certificate-authorities",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
"/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
),
AlertingPage: setRoute(
"/projects/cert-management/$projectId/alerting",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting"
"/organizations/$orgId/projects/cert-management/$projectId/alerting",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/alerting"
),
PkiCollectionDetailsByIDPage: setRoute(
"/projects/cert-management/$projectId/pki-collections/$collectionId",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/pki-collections/$collectionId"
"/organizations/$orgId/projects/cert-management/$projectId/pki-collections/$collectionId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/pki-collections/$collectionId"
),
PkiSubscriberDetailsByIDPage: setRoute(
"/projects/cert-management/$projectId/subscribers/$subscriberName",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName"
"/organizations/$orgId/projects/cert-management/$projectId/subscribers/$subscriberName",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName"
),
IntegrationsListPage: setRoute(
"/projects/cert-management/$projectId/integrations",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/"
"/organizations/$orgId/projects/cert-management/$projectId/integrations",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/"
),
PkiSyncDetailsByIDPage: setRoute(
"/projects/cert-management/$projectId/integrations/$syncId",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId"
"/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId"
)
},
Ssh: {
SshCaByIDPage: setRoute(
"/projects/ssh/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ca/$caId"
"/organizations/$orgId/projects/ssh/$projectId/ca/$caId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ssh/$projectId/_ssh-layout/ca/$caId"
),
SshHostGroupDetailsByIDPage: setRoute(
"/projects/ssh/$projectId/ssh-host-groups/$sshHostGroupId",
"/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
"/organizations/$orgId/projects/ssh/$projectId/ssh-host-groups/$sshHostGroupId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId"
)
},
SecretScanning: {
DataSourceByIdPage: setRoute(
"/projects/secret-scanning/$projectId/data-sources/$type/$dataSourceId",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId"
"/organizations/$orgId/projects/secret-scanning/$projectId/data-sources/$type/$dataSourceId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId"
),
FindingsPage: setRoute(
"/projects/secret-scanning/$projectId/findings",
"/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings"
"/organizations/$orgId/projects/secret-scanning/$projectId/findings",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/findings"
)
},
Pam: {
AccountsPage: setRoute(
"/projects/pam/$projectId/accounts",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts"
"/organizations/$orgId/projects/pam/$projectId/accounts",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/accounts"
),
ResourcesPage: setRoute(
"/projects/pam/$projectId/resources",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources"
"/organizations/$orgId/projects/pam/$projectId/resources",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/resources"
),
SessionsPage: setRoute(
"/projects/pam/$projectId/sessions",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/"
"/organizations/$orgId/projects/pam/$projectId/sessions",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/"
),
PamSessionByIDPage: setRoute(
"/projects/pam/$projectId/sessions/$sessionId",
"/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
"/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId",
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
)
},
Public: {

View File

@@ -49,6 +49,7 @@ import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/b
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection";
import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection";
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 { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-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.GitLab]: { name: "GitLab", image: "GitLab.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.Railway]: { name: "Railway", image: "Railway.png" },
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
@@ -214,6 +216,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
return { name: "Client Secret", icon: faKey };
case AzureClientSecretsConnectionMethod.Certificate:
return { name: "Certificate", icon: faCertificate };
case DNSMadeEasyConnectionMethod.APIKeySecret:
return { name: "API Key & Secret", icon: faKey };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}

View File

@@ -64,11 +64,11 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
export const getProjectBaseURL = (type: ProjectType) => {
switch (type) {
case ProjectType.SecretManager:
return "/projects/secret-management/$projectId";
return "/organizations/$orgId/projects/secret-management/$projectId";
case ProjectType.CertificateManager:
return "/projects/cert-management/$projectId";
return "/organizations/$orgId/projects/cert-management/$projectId";
default:
return `/projects/${type}/$projectId` as const;
return `/organizations/$orgId/projects/${type}/$projectId` as const;
}
};
@@ -77,15 +77,15 @@ export const getProjectBaseURL = (type: ProjectType) => {
export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[]) => {
switch (type) {
case ProjectType.SecretManager:
return "/projects/secret-management/$projectId/overview" as const;
return "/organizations/$orgId/projects/secret-management/$projectId/overview" as const;
case ProjectType.CertificateManager:
return "/projects/cert-management/$projectId/policies" as const;
return "/organizations/$orgId/projects/cert-management/$projectId/policies" as const;
case ProjectType.SecretScanning:
return `/projects/${type}/$projectId/data-sources` as const;
return `/organizations/$orgId/projects/${type}/$projectId/data-sources` as const;
case ProjectType.PAM:
return `/projects/${type}/$projectId/accounts` as const;
return `/organizations/$orgId/projects/${type}/$projectId/accounts` as const;
default:
return `/projects/${type}/$projectId/overview` as const;
return `/organizations/$orgId/projects/${type}/$projectId/overview` as const;
}
};

View File

@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";

Some files were not shown because too many files have changed in this diff Show More