mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
Merge remote-tracking branch 'origin/main' into misc/revamp-pki-apis
This commit is contained in:
@@ -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"} |
|
||||
| {} |
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -776,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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateDNSMadeEasyConnectionSchema,
|
||||
SanitizedDNSMadeEasyConnectionSchema,
|
||||
UpdateDNSMadeEasyConnectionSchema
|
||||
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerDNSMadeEasyConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.DNSMadeEasy,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedDNSMadeEasyConnectionSchema,
|
||||
createSchema: CreateDNSMadeEasyConnectionSchema,
|
||||
updateSchema: UpdateDNSMadeEasyConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/dns-made-easy-zones`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const zones = await server.services.appConnection.dnsMadeEasy.listZones(connectionId, req.permission);
|
||||
return zones;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||
import { 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,
|
||||
|
||||
@@ -29,6 +29,7 @@ export enum AppConnection {
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway",
|
||||
Bitbucket = "bitbucket",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum DNSMadeEasyConnectionMethod {
|
||||
APIKeySecret = "api-key-secret"
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
|
||||
import {
|
||||
TDNSMadeEasyConnection,
|
||||
TDNSMadeEasyConnectionConfig,
|
||||
TDNSMadeEasyZone
|
||||
} from "./dns-made-easy-connection-types";
|
||||
|
||||
interface DNSMadeEasyApiResponse {
|
||||
totalRecords: number;
|
||||
totalPages: number;
|
||||
data: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}>;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export const getDNSMadeEasyUrl = (path: string) => {
|
||||
const appCfg = getConfig();
|
||||
return `${appCfg.DNS_MADE_EASY_SANDBOX_ENABLED ? IntegrationUrls.DNS_MADE_EASY_SANDBOX_API_URL : IntegrationUrls.DNS_MADE_EASY_API_URL}${path}`;
|
||||
};
|
||||
|
||||
export const makeDNSMadeEasyAuthHeaders = (
|
||||
apiKey: string,
|
||||
secretKey: string,
|
||||
currentDate?: Date
|
||||
): Record<string, string> => {
|
||||
// Format date as "Day, DD Mon YYYY HH:MM:SS GMT" (e.g., "Mon, 01 Jan 2024 12:00:00 GMT")
|
||||
const requestDate = (currentDate ?? new Date()).toUTCString();
|
||||
|
||||
// Generate HMAC-SHA1 signature
|
||||
const hmac = crypto.nativeCrypto.createHmac("sha1", secretKey);
|
||||
hmac.update(requestDate);
|
||||
const hmacSignature = hmac.digest("hex");
|
||||
|
||||
return {
|
||||
"x-dnsme-apiKey": apiKey,
|
||||
"x-dnsme-hmac": hmacSignature,
|
||||
"x-dnsme-requestDate": requestDate
|
||||
};
|
||||
};
|
||||
|
||||
export const getDNSMadeEasyConnectionListItem = () => {
|
||||
return {
|
||||
name: "DNS Made Easy" as const,
|
||||
app: AppConnection.DNSMadeEasy as const,
|
||||
methods: Object.values(DNSMadeEasyConnectionMethod) as [DNSMadeEasyConnectionMethod.APIKeySecret]
|
||||
};
|
||||
};
|
||||
|
||||
export const listDNSMadeEasyZones = async (appConnection: TDNSMadeEasyConnection): Promise<TDNSMadeEasyZone[]> => {
|
||||
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = appConnection;
|
||||
|
||||
try {
|
||||
const allZones: TDNSMadeEasyZone[] = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 1;
|
||||
|
||||
// Fetch all pages of zones
|
||||
while (currentPage < totalPages) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resp = await request.get<DNSMadeEasyApiResponse>(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: {
|
||||
page: currentPage
|
||||
}
|
||||
});
|
||||
|
||||
if (resp.data?.data) {
|
||||
// Map the API response to TDNSMadeEasyZone format
|
||||
const zones = resp.data.data.map((zone) => ({
|
||||
id: String(zone.id),
|
||||
name: zone.name
|
||||
}));
|
||||
allZones.push(...zones);
|
||||
|
||||
// Update pagination info
|
||||
totalPages = resp.data.totalPages || 1;
|
||||
currentPage += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allZones;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error listing DNS Made Easy zones");
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to list DNS Made Easy zones: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list DNS Made Easy zones"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const listDNSMadeEasyRecords = async (
|
||||
appConnection: TDNSMadeEasyConnection,
|
||||
options: { zoneId: string; type?: string; name?: string }
|
||||
): Promise<DNSMadeEasyApiResponse["data"]> => {
|
||||
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = appConnection;
|
||||
const { zoneId, type, name } = options;
|
||||
|
||||
try {
|
||||
const allRecords: DNSMadeEasyApiResponse["data"] = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 1;
|
||||
|
||||
// Fetch all pages of records
|
||||
while (currentPage < totalPages) {
|
||||
// Build query parameters
|
||||
const queryParams: Record<string, string | number> = {};
|
||||
if (type) {
|
||||
queryParams.type = type;
|
||||
}
|
||||
if (name) {
|
||||
queryParams.recordName = name;
|
||||
}
|
||||
queryParams.page = currentPage;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resp = await request.get<DNSMadeEasyApiResponse>(
|
||||
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(zoneId)}/records`),
|
||||
{
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: queryParams
|
||||
}
|
||||
);
|
||||
|
||||
if (resp.data?.data) {
|
||||
allRecords.push(...resp.data.data);
|
||||
|
||||
// Update pagination info
|
||||
totalPages = resp.data.totalPages || 1;
|
||||
currentPage += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allRecords;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error listing DNS Made Easy records");
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to list DNS Made Easy records: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list DNS Made Easy records"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateDNSMadeEasyConnectionCredentials = async (config: TDNSMadeEasyConnectionConfig) => {
|
||||
if (config.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
|
||||
const { apiKey, secretKey } = config.credentials;
|
||||
|
||||
try {
|
||||
const resp = await request.get(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
if (resp.status !== 200) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: Invalid API credentials provided."
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to validate credentials: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
logger.error(error, "Error validating DNS Made Easy connection credentials");
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
|
||||
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
|
||||
|
||||
export const DNSMadeEasyConnectionApiKeyCredentialsSchema = z.object({
|
||||
apiKey: z.string().trim().min(1, "API key required").max(256, "API key cannot exceed 256 characters"),
|
||||
secretKey: z.string().trim().min(1, "Secret key required").max(256, "Secret key cannot exceed 256 characters")
|
||||
});
|
||||
|
||||
const BaseDNSMadeEasyConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.DNSMadeEasy)
|
||||
});
|
||||
|
||||
export const DNSMadeEasyConnectionSchema = BaseDNSMadeEasyConnectionSchema.extend({
|
||||
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedDNSMadeEasyConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseDNSMadeEasyConnectionSchema.extend({
|
||||
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.pick({ apiKey: true })
|
||||
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy]} (API Key)` }))
|
||||
]);
|
||||
|
||||
export const ValidateDNSMadeEasyConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(DNSMadeEasyConnectionMethod.APIKeySecret)
|
||||
.describe(AppConnections.CREATE(AppConnection.DNSMadeEasy).method),
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.DNSMadeEasy).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateDNSMadeEasyConnectionSchema = ValidateDNSMadeEasyConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy)
|
||||
);
|
||||
|
||||
export const UpdateDNSMadeEasyConnectionSchema = z
|
||||
.object({
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.DNSMadeEasy).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy));
|
||||
|
||||
export const DNSMadeEasyConnectionListItemSchema = z
|
||||
.object({
|
||||
name: z.literal("DNS Made Easy"),
|
||||
app: z.literal(AppConnection.DNSMadeEasy),
|
||||
methods: z.nativeEnum(DNSMadeEasyConnectionMethod).array()
|
||||
})
|
||||
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy] }));
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listDNSMadeEasyZones } from "./dns-made-easy-connection-fns";
|
||||
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TDNSMadeEasyConnection>;
|
||||
|
||||
export const dnsMadeEasyConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listZones = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.DNSMadeEasy, connectionId, actor);
|
||||
try {
|
||||
const zones = await listDNSMadeEasyZones(appConnection);
|
||||
return zones;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
`Failed to list DNS Made Easy zones for DNS Made Easy connection [connectionId=${connectionId}]`
|
||||
);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list DNS Made Easy zones: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listZones
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateDNSMadeEasyConnectionSchema,
|
||||
DNSMadeEasyConnectionSchema,
|
||||
ValidateDNSMadeEasyConnectionCredentialsSchema
|
||||
} from "./dns-made-easy-connection-schema";
|
||||
|
||||
export type TDNSMadeEasyConnection = z.infer<typeof DNSMadeEasyConnectionSchema>;
|
||||
|
||||
export type TDNSMadeEasyConnectionInput = z.infer<typeof CreateDNSMadeEasyConnectionSchema> & {
|
||||
app: AppConnection.DNSMadeEasy;
|
||||
};
|
||||
|
||||
export type TValidateDNSMadeEasyConnectionCredentialsSchema = typeof ValidateDNSMadeEasyConnectionCredentialsSchema;
|
||||
|
||||
export type TDNSMadeEasyConnectionConfig = DiscriminativePick<
|
||||
TDNSMadeEasyConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TDNSMadeEasyZone = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum AcmeDnsProvider {
|
||||
Route53 = "route53",
|
||||
Cloudflare = "cloudflare"
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { 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}`);
|
||||
}
|
||||
@@ -411,6 +453,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,
|
||||
@@ -504,6 +552,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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -752,7 +752,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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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/).
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
267
docs/documentation/platform/pki/k8s-cert-manager.mdx
Normal file
267
docs/documentation/platform/pki/k8s-cert-manager.mdx
Normal 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>
|
||||
@@ -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.
|
Before Width: | Height: | Size: 358 KiB After Width: | Height: | Size: 368 KiB |
80
frontend/public/images/integrations/DNSMadeEasy.svg
Normal file
80
frontend/public/images/integrations/DNSMadeEasy.svg
Normal 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 |
@@ -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 = (
|
||||
|
||||
@@ -57,6 +57,7 @@ import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-con
|
||||
import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection";
|
||||
import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection";
|
||||
import { SupabaseConnectionMethod } from "@app/hooks/api/appConnections/types/supabase-connection";
|
||||
import { DNSMadeEasyConnectionMethod } from "@app/hooks/api/appConnections/types/dns-made-easy-connection";
|
||||
|
||||
export const APP_CONNECTION_MAP: Record<
|
||||
AppConnection,
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import { TDNSMadeEasyZone } from "./types";
|
||||
|
||||
const dnsMadeEasyConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "dns-made-easy"] as const,
|
||||
listZones: (connectionId: string) =>
|
||||
[...dnsMadeEasyConnectionKeys.all, "zones", connectionId] as const
|
||||
};
|
||||
|
||||
export const useDNSMadeEasyConnectionListZones = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TDNSMadeEasyZone[],
|
||||
unknown,
|
||||
TDNSMadeEasyZone[],
|
||||
ReturnType<typeof dnsMadeEasyConnectionKeys.listZones>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: dnsMadeEasyConnectionKeys.listZones(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TDNSMadeEasyZone[]>(
|
||||
`/api/v1/app-connections/dns-made-easy/${connectionId}/dns-made-easy-zones`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type TDNSMadeEasyZone = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export enum AppConnection {
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy",
|
||||
Bitbucket = "bitbucket",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway",
|
||||
|
||||
@@ -184,6 +184,10 @@ export type TRedisConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Redis;
|
||||
};
|
||||
|
||||
export type TDNSMadeEasyConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.DNSMadeEasy;
|
||||
};
|
||||
|
||||
export type TAppConnectionOption =
|
||||
| TAwsConnectionOption
|
||||
| TGitHubConnectionOption
|
||||
@@ -225,7 +229,8 @@ export type TAppConnectionOption =
|
||||
| TOktaConnectionOption
|
||||
| TAzureAdCsConnectionOption
|
||||
| TLaravelForgeConnectionOption
|
||||
| TChefConnectionOption;
|
||||
| TChefConnectionOption
|
||||
| TDNSMadeEasyConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
@@ -257,6 +262,7 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.Flyio]: TFlyioConnectionOption;
|
||||
[AppConnection.GitLab]: TGitlabConnectionOption;
|
||||
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
|
||||
[AppConnection.DNSMadeEasy]: TDNSMadeEasyConnectionOption;
|
||||
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
||||
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
||||
[AppConnection.Railway]: TRailwayConnectionOption;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum DNSMadeEasyConnectionMethod {
|
||||
APIKeySecret = "api-key-secret"
|
||||
}
|
||||
|
||||
export type TDNSMadeEasyConnection = TRootAppConnection & { app: AppConnection.DNSMadeEasy } & {
|
||||
method: DNSMadeEasyConnectionMethod.APIKeySecret;
|
||||
credentials: {
|
||||
apiKey: string;
|
||||
secretKey: string;
|
||||
};
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { TChefConnection } from "./chef-connection";
|
||||
import { TCloudflareConnection } from "./cloudflare-connection";
|
||||
import { TDatabricksConnection } from "./databricks-connection";
|
||||
import { TDigitalOceanConnection } from "./digital-ocean";
|
||||
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection";
|
||||
import { TFlyioConnection } from "./flyio-connection";
|
||||
import { TGcpConnection } from "./gcp-connection";
|
||||
import { TGitHubConnection } from "./github-connection";
|
||||
@@ -56,6 +57,7 @@ export * from "./camunda-connection";
|
||||
export * from "./checkly-connection";
|
||||
export * from "./chef-connection";
|
||||
export * from "./cloudflare-connection";
|
||||
export * from "./dns-made-easy-connection";
|
||||
export * from "./databricks-connection";
|
||||
export * from "./flyio-connection";
|
||||
export * from "./gcp-connection";
|
||||
@@ -127,7 +129,8 @@ export type TAppConnection =
|
||||
| TNorthflankConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
| TChefConnection;
|
||||
| TChefConnection
|
||||
| TDNSMadeEasyConnection;
|
||||
|
||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
|
||||
|
||||
|
||||
@@ -16,12 +16,14 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
||||
|
||||
export const ACME_DNS_PROVIDER_NAME_MAP: Record<AcmeDnsProvider, string> = {
|
||||
[AcmeDnsProvider.ROUTE53]: "Route53",
|
||||
[AcmeDnsProvider.Cloudflare]: "Cloudflare"
|
||||
[AcmeDnsProvider.Cloudflare]: "Cloudflare",
|
||||
[AcmeDnsProvider.DNSMadeEasy]: "DNS Made Easy"
|
||||
};
|
||||
|
||||
export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppConnection> = {
|
||||
[AcmeDnsProvider.ROUTE53]: AppConnection.AWS,
|
||||
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare
|
||||
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare,
|
||||
[AcmeDnsProvider.DNSMadeEasy]: AppConnection.DNSMadeEasy
|
||||
};
|
||||
|
||||
export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
||||
|
||||
@@ -21,7 +21,8 @@ export enum CaRenewalType {
|
||||
|
||||
export enum AcmeDnsProvider {
|
||||
ROUTE53 = "route53",
|
||||
Cloudflare = "cloudflare"
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy"
|
||||
}
|
||||
|
||||
export enum CaCapability {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ResourceOverviewPage = () => {
|
||||
Users
|
||||
</Tab>
|
||||
<Tab variant="instance" value="tab-identities">
|
||||
Identities
|
||||
Machine Identities
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel value="tab-organizations">
|
||||
|
||||
@@ -90,7 +90,7 @@ const IdentityPanelTable = ({
|
||||
value={searchIdentityFilter}
|
||||
onChange={(e) => setSearchIdentityFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search identities by name..."
|
||||
placeholder="Search machine identities by name..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
TCloudflareZone,
|
||||
useCloudflareConnectionListZones
|
||||
} from "@app/hooks/api/appConnections/cloudflare";
|
||||
import {
|
||||
TDNSMadeEasyZone,
|
||||
useDNSMadeEasyConnectionListZones
|
||||
} from "@app/hooks/api/appConnections/dns-made-easy";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
AcmeDnsProvider,
|
||||
@@ -210,6 +214,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
enabled: caType === CaType.ACME
|
||||
});
|
||||
|
||||
const { data: availableDNSMadeEasyConnections, isPending: isDNSMadeEasyPending } =
|
||||
useListAvailableAppConnections(AppConnection.DNSMadeEasy, currentProject.id, {
|
||||
enabled: caType === CaType.ACME
|
||||
});
|
||||
|
||||
const { data: availableAzureConnections, isPending: isAzurePending } =
|
||||
useListAvailableAppConnections(AppConnection.AzureADCS, currentProject.id, {
|
||||
enabled: caType === CaType.AZURE_AD_CS
|
||||
@@ -219,16 +228,24 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (caType === CaType.AZURE_AD_CS) {
|
||||
return availableAzureConnections || [];
|
||||
}
|
||||
return [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])];
|
||||
return [
|
||||
...(availableRoute53Connections || []),
|
||||
...(availableCloudflareConnections || []),
|
||||
...(availableDNSMadeEasyConnections || [])
|
||||
];
|
||||
}, [
|
||||
caType,
|
||||
availableRoute53Connections,
|
||||
availableCloudflareConnections,
|
||||
availableDNSMadeEasyConnections,
|
||||
availableAzureConnections
|
||||
]);
|
||||
|
||||
const isPending =
|
||||
isRoute53Pending || isCloudflarePending || (isAzurePending && caType === CaType.AZURE_AD_CS);
|
||||
isRoute53Pending ||
|
||||
isCloudflarePending ||
|
||||
isDNSMadeEasyPending ||
|
||||
(isAzurePending && caType === CaType.AZURE_AD_CS);
|
||||
|
||||
const dnsAppConnection =
|
||||
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
|
||||
@@ -240,6 +257,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id
|
||||
});
|
||||
|
||||
const { data: dnsMadeEasyZones = [], isPending: isDNSMadeEasyZonesPending } =
|
||||
useDNSMadeEasyConnectionListZones(dnsAppConnection.id, {
|
||||
enabled: dnsProvider === AcmeDnsProvider.DNSMadeEasy && !!dnsAppConnection.id
|
||||
});
|
||||
|
||||
// Populate form with CA data when editing
|
||||
useEffect(() => {
|
||||
if (ca && !isCaLoading) {
|
||||
@@ -499,6 +521,32 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{dnsProvider === AcmeDnsProvider.DNSMadeEasy && (
|
||||
<Controller
|
||||
name="configuration.dnsProviderConfig.hostedZoneId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Zone"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isDNSMadeEasyZonesPending && !!dnsAppConnection.id}
|
||||
isDisabled={!dnsAppConnection.id}
|
||||
value={dnsMadeEasyZones.find((zone) => zone.id === value)}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TDNSMadeEasyZone>)?.id ?? null);
|
||||
}}
|
||||
options={dnsMadeEasyZones}
|
||||
placeholder="Select a zone..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
|
||||
@@ -577,7 +577,7 @@ export const CreateProfileModal = ({
|
||||
isDisabled={Boolean(isEdit)}
|
||||
>
|
||||
<SelectItem value="ca">Certificate Authority</SelectItem>
|
||||
<SelectItem value="self-signed">Self-signed</SelectItem>
|
||||
<SelectItem value="self-signed">Self-Signed</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
@@ -58,7 +58,7 @@ export const AccessManagementPage = () => {
|
||||
},
|
||||
{
|
||||
key: OrgAccessControlTabSections.Identities,
|
||||
label: "Identities",
|
||||
label: "Machine Identities",
|
||||
isHidden: permission.cannot(
|
||||
OrgPermissionIdentityActions.Read,
|
||||
OrgPermissionSubjects.Identity
|
||||
@@ -84,7 +84,7 @@ export const AccessManagementPage = () => {
|
||||
<PageHeader
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
title="Access Control"
|
||||
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
|
||||
description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources."
|
||||
/>
|
||||
{!currentOrg.shouldUseNewPrivilegeSystem && (
|
||||
<div className="mt-4 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
|
||||
|
||||
@@ -154,7 +154,9 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
|
||||
onOpenChange={handleClose}
|
||||
>
|
||||
<ModalContent
|
||||
title={isEdit ? "Edit Identity Auth Template" : "Create Identity Auth Template"}
|
||||
title={
|
||||
isEdit ? "Edit Machine Identity Auth Template" : "Create Machine Identity Auth Template"
|
||||
}
|
||||
subTitle={
|
||||
isEdit ? "Update the authentication template" : "Create a new authentication template"
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export const IdentitySection = withPermission(
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted identity",
|
||||
text: "Successfully deleted machine identity",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ export const IdentitySection = withPermission(
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-1 items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
|
||||
<p className="text-xl font-medium text-mineshaft-100">Machine Identities</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
@@ -116,7 +116,7 @@ export const IdentitySection = withPermission(
|
||||
if (!isMoreIdentitiesAllowed && !isEnterprise) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"You can add more identities if you upgrade your Infisical Pro plan."
|
||||
"You can add more machine identities if you upgrade your Infisical Pro plan."
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -129,7 +129,7 @@ export const IdentitySection = withPermission(
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create Identity
|
||||
Add Machine Identity
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
@@ -141,7 +141,9 @@ export const IdentitySection = withPermission(
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identity Auth Templates</p>
|
||||
<p className="text-xl font-medium text-mineshaft-100">
|
||||
Machine Identity Auth Templates
|
||||
</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/auth-templates" />
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
@@ -150,14 +152,14 @@ export const IdentitySection = withPermission(
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (subscription && !subscription.machineIdentityAuthTemplates) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
isEnterpriseFeature: true,
|
||||
text: "Your current plan does not include access to creating Identity Auth Templates. To unlock this feature, please upgrade to Infisical Enterprise plan."
|
||||
text: "Your current plan does not include access to creating Machine Identity Auth Templates. To unlock this feature, please upgrade to Infisical Enterprise plan."
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -197,9 +199,11 @@ export const IdentitySection = withPermission(
|
||||
>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
title="Add Identity"
|
||||
title="Add Machine Identity"
|
||||
subTitle={
|
||||
isSubOrganization ? "Create a new identity or assign an existing identity" : undefined
|
||||
isSubOrganization
|
||||
? "Create a new machine identity or assign an existing one"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
@@ -224,11 +228,11 @@ export const IdentitySection = withPermission(
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<PlusIcon size="1rem" />
|
||||
<div>Create New Identity</div>
|
||||
<div>Create Machine Identity</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-mineshaft-300">
|
||||
Create a new machine identity specifically for this sub-organization. This
|
||||
identity will be managed at the sub-organization level.
|
||||
machine identity will be managed at the sub-organization level.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -244,11 +248,11 @@ export const IdentitySection = withPermission(
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkIcon size="1rem" />
|
||||
<div>Assign Existing Identity</div>
|
||||
<div>Assign Existing Machine Identity</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-mineshaft-300">
|
||||
Assign an existing identity from your parent organization. The identity will
|
||||
continue to be managed at its original scope.
|
||||
Assign an existing machine identity from your parent organization. The machine
|
||||
identity will continue to be managed at its original scope.
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -152,7 +152,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated identity role",
|
||||
text: "Successfully updated machine identity role",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
@@ -178,7 +178,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Filter Identities"
|
||||
ariaLabel="Filter Machine Identities"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
@@ -200,7 +200,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
</DropdownSubMenuTrigger>
|
||||
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Apply Roles to Filter Identities
|
||||
Filter Machine Identities by Role
|
||||
</DropdownMenuLabel>
|
||||
{roles?.map(({ id, slug, name }) => (
|
||||
<DropdownMenuItem
|
||||
@@ -229,7 +229,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search identities by name..."
|
||||
placeholder="Search machine identities by name..."
|
||||
/>
|
||||
</div>
|
||||
<TableContainer>
|
||||
@@ -407,7 +407,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Edit Identity {isSubOrgIdentity ? "" : "Membership"}
|
||||
Edit Machine Identity {isSubOrgIdentity ? "" : "Membership"}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
@@ -428,7 +428,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
{isSubOrgIdentity
|
||||
? "Delete Identity"
|
||||
? "Delete Machine Identity"
|
||||
: "Remove From Sub-Organization"}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
@@ -455,8 +455,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
<EmptyState
|
||||
title={
|
||||
debouncedSearch.trim().length > 0 || filter.roles?.length > 0
|
||||
? "No identities match search filter"
|
||||
: "No identities have been created in this organization"
|
||||
? "No machine identities match search filter"
|
||||
: "No machine identities have been created in this organization"
|
||||
}
|
||||
icon={faServer}
|
||||
/>
|
||||
|
||||
@@ -78,11 +78,11 @@ export const OrgIdentityLinkForm = ({ onClose }: Props) => {
|
||||
control={control}
|
||||
name="identity"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||
<FormControl label="Machine Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select identity..."
|
||||
placeholder="Select machine identity..."
|
||||
// onInputChange={setSearchValue}
|
||||
options={rootOrgIdentities}
|
||||
getOptionValue={(option) => option.id}
|
||||
|
||||
@@ -164,7 +164,7 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${popUp?.identity?.data ? "updated" : "created"} identity`,
|
||||
text: `Successfully ${popUp?.identity?.data ? "updated" : "created"} machine identity`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
|
||||
@@ -159,14 +159,16 @@ export const AddOrgMemberModal = ({
|
||||
)
|
||||
);
|
||||
|
||||
setCompleteInviteLinks(data?.completeInviteLinks ?? null);
|
||||
if (data?.completeInviteLinks && data?.completeInviteLinks.length > 0) {
|
||||
setCompleteInviteLinks(data.completeInviteLinks);
|
||||
}
|
||||
|
||||
// only show this notification when email is configured.
|
||||
// A [completeInviteLink] will not be sent if smtp is configured
|
||||
|
||||
if (!data.completeInviteLinks) {
|
||||
if (!data.completeInviteLinks?.length) {
|
||||
createNotification({
|
||||
text: "Successfully invited user to the organization.",
|
||||
text: `Successfully invited user${usernames.length > 1 ? "s" : ""} to the organization.`,
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ChefConnectionForm } from "./ChefConnectionForm";
|
||||
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
|
||||
import { DNSMadeEasyConnectionForm } from "./DNSMadeEasyConnectionForm";
|
||||
import { FlyioConnectionForm } from "./FlyioConnectionForm";
|
||||
import { GcpConnectionForm } from "./GcpConnectionForm";
|
||||
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
||||
@@ -148,6 +149,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
|
||||
return <GitLabConnectionForm onSubmit={onSubmit} projectId={projectId} />;
|
||||
case AppConnection.Cloudflare:
|
||||
return <CloudflareConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.DNSMadeEasy:
|
||||
return <DNSMadeEasyConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Bitbucket:
|
||||
return <BitbucketConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Zabbix:
|
||||
@@ -306,6 +309,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
);
|
||||
case AppConnection.Cloudflare:
|
||||
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.DNSMadeEasy:
|
||||
return <DNSMadeEasyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Bitbucket:
|
||||
return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Zabbix:
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { TDNSMadeEasyConnection } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import { DNSMadeEasyConnectionMethod } from "@app/hooks/api/appConnections/types/dns-made-easy-connection";
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TDNSMadeEasyConnection;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.DNSMadeEasy)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||
credentials: z.object({
|
||||
apiKey: z.string().trim().min(1, "API Key required"),
|
||||
secretKey: z.string().trim().min(1, "Secret Key required")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const DNSMadeEasyConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.DNSMadeEasy,
|
||||
method: DNSMadeEasyConnectionMethod.APIKeySecret,
|
||||
credentials: {
|
||||
apiKey: "",
|
||||
secretKey: ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.DNSMadeEasy].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(DNSMadeEasyConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.apiKey"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="API Key"
|
||||
>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="af1b628f-3272-46aa-9cde-837d0c59155d"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.secretKey"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Secret Key"
|
||||
>
|
||||
<SecretInput
|
||||
containerClassName="text-gray-400 group-focus-within:border-primary-400/50! border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Credentials" : "Connect to DNS Made Easy"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
@@ -55,7 +55,7 @@ const Page = () => {
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted identity",
|
||||
text: "Successfully deleted machine identity",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -82,11 +82,11 @@ const Page = () => {
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Identities
|
||||
Machine Identities
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
description={`${isSubOrganization ? "Sub-" : ""}Organization Identity`}
|
||||
description={`${isSubOrganization ? "Sub-" : ""}Organization Machine Identity`}
|
||||
title={data.identity.name}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -111,7 +111,7 @@ const Page = () => {
|
||||
})
|
||||
}
|
||||
>
|
||||
Unlink Identity
|
||||
Unlink Machine Identity
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
@@ -142,7 +142,7 @@ const Page = () => {
|
||||
>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`}
|
||||
title={`${popUp?.identity?.data ? "Update" : "Create"} Machine Identity`}
|
||||
>
|
||||
<OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</ModalContent>
|
||||
|
||||
@@ -45,7 +45,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdent
|
||||
return data ? (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Identity Details</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Details</h3>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -86,7 +86,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdent
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
{isOrgIdentity ? "Edit Identity" : "Edit Identity Role"}
|
||||
{isOrgIdentity ? "Edit Machine Identity" : "Edit Machine Identity Role"}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
@@ -110,7 +110,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdent
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
{!isOrgIdentity ? "Remove From Sub-Organization" : "Delete Identity"}
|
||||
{!isOrgIdentity ? "Remove From Sub-Organization" : "Delete Machine Identity"}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
@@ -119,7 +119,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdent
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Identity ID</p>
|
||||
<p className="text-sm font-medium text-mineshaft-300">Machine Identity ID</p>
|
||||
<div className="group flex align-top">
|
||||
<p className="text-sm text-mineshaft-300">{data.identity.id}</p>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
|
||||
@@ -167,7 +167,7 @@ export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle
|
||||
handlePopUpToggle("addIdentityToProject", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent bodyClassName="overflow-visible" title="Add Identity to Project">
|
||||
<ModalContent bodyClassName="overflow-visible" title="Add Machine Identity to Project">
|
||||
<Content identityId={identityId} handlePopUpToggle={handlePopUpToggle} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -151,7 +151,7 @@ export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) =>
|
||||
title={
|
||||
projectMemberships.length
|
||||
? "No projects match search..."
|
||||
: "This identity has not been assigned to any projects"
|
||||
: "This machine identity has not been assigned to any projects"
|
||||
}
|
||||
icon={projectMemberships.length ? faSearch : faFolder}
|
||||
/>
|
||||
|
||||
@@ -55,10 +55,10 @@ const formSchemaWithIdentity = baseFormSchema.extend({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
},
|
||||
{ required_error: "Identity is required" }
|
||||
{ required_error: "Machine identity is required" }
|
||||
)
|
||||
.nullable()
|
||||
.refine((val) => val !== null, { message: "Identity is required" })
|
||||
.refine((val) => val !== null, { message: "Machine identity is required" })
|
||||
});
|
||||
|
||||
const formSchemaWithToken = baseFormSchema.extend({
|
||||
@@ -275,8 +275,8 @@ export const GatewayCliDeploymentMethod = () => {
|
||||
{canCreateToken && autogenerateToken ? (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity"
|
||||
tooltipText="The identity that your gateway will use for authentication."
|
||||
label="Machine Identity"
|
||||
tooltipText="The machine identity that your gateway will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
@@ -290,7 +290,7 @@ export const GatewayCliDeploymentMethod = () => {
|
||||
)
|
||||
}
|
||||
isLoading={isIdentitiesLoading}
|
||||
placeholder="Select identity..."
|
||||
placeholder="Select machine identity..."
|
||||
options={identityMembershipOrgs.map((membership) => membership.identity)}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
@@ -300,14 +300,14 @@ export const GatewayCliDeploymentMethod = () => {
|
||||
) : (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity Token"
|
||||
tooltipText="The identity token that your gateway will use for authentication."
|
||||
label="Machine Identity Token"
|
||||
tooltipText="The machine identity token that your gateway will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={identityToken}
|
||||
onChange={(e) => setIdentityToken(e.target.value)}
|
||||
placeholder="Enter identity token..."
|
||||
placeholder="Enter machine identity token..."
|
||||
isError={Boolean(errors.identityToken)}
|
||||
/>
|
||||
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
|
||||
@@ -325,15 +325,15 @@ export const GatewayCliDeploymentMethod = () => {
|
||||
className="mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>Automatically enable token auth and generate a token for identity</span>
|
||||
<span>Automatically enable token auth and generate a token for machine identity</span>
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
Token authentication will be automatically enabled for the selected identity if
|
||||
it isn't already configured. By default, it will be configured to allow all
|
||||
IP addresses with a token TTL of 30 days. You can manage these settings in
|
||||
Access Control.
|
||||
Token authentication will be automatically enabled for the selected machine
|
||||
identity if it isn't already configured. By default, it will be configured
|
||||
to allow all IP addresses with a token TTL of 30 days. You can manage these
|
||||
settings in Access Control.
|
||||
<br />
|
||||
<br />A token will automatically be generated to be used with the CLI command.
|
||||
</>
|
||||
|
||||
@@ -55,10 +55,10 @@ const formSchemaWithIdentity = baseFormSchema.extend({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
},
|
||||
{ required_error: "Identity is required" }
|
||||
{ required_error: "Machine identity is required" }
|
||||
)
|
||||
.nullable()
|
||||
.refine((val) => val !== null, { message: "Identity is required" })
|
||||
.refine((val) => val !== null, { message: "Machine identity is required" })
|
||||
});
|
||||
|
||||
const formSchemaWithToken = baseFormSchema.extend({
|
||||
@@ -297,8 +297,8 @@ export const GatewayCliSystemdDeploymentMethod = () => {
|
||||
{canCreateToken && autogenerateToken ? (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity"
|
||||
tooltipText="The identity that your gateway will use for authentication."
|
||||
label="Machine Identity"
|
||||
tooltipText="The machine identity that your gateway will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
@@ -312,7 +312,7 @@ export const GatewayCliSystemdDeploymentMethod = () => {
|
||||
)
|
||||
}
|
||||
isLoading={isIdentitiesLoading}
|
||||
placeholder="Select identity..."
|
||||
placeholder="Select machine identity..."
|
||||
options={identityMembershipOrgs.map((membership) => membership.identity)}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
@@ -322,14 +322,14 @@ export const GatewayCliSystemdDeploymentMethod = () => {
|
||||
) : (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity Token"
|
||||
tooltipText="The identity token that your gateway will use for authentication."
|
||||
label="Machine Identity Token"
|
||||
tooltipText="The machine identity token that your gateway will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={identityToken}
|
||||
onChange={(e) => setIdentityToken(e.target.value)}
|
||||
placeholder="Enter identity token..."
|
||||
placeholder="Enter machine identity token..."
|
||||
isError={Boolean(errors.identityToken)}
|
||||
/>
|
||||
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
|
||||
@@ -347,15 +347,15 @@ export const GatewayCliSystemdDeploymentMethod = () => {
|
||||
className="mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>Automatically enable token auth and generate a token for identity</span>
|
||||
<span>Automatically enable token auth and generate a token for machine identity</span>
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
Token authentication will be automatically enabled for the selected identity if
|
||||
it isn't already configured. By default, it will be configured to allow all
|
||||
IP addresses with a token TTL of 30 days. You can manage these settings in
|
||||
Access Control.
|
||||
Token authentication will be automatically enabled for the selected machine
|
||||
identity if it isn't already configured. By default, it will be configured
|
||||
to allow all IP addresses with a token TTL of 30 days. You can manage these
|
||||
settings in Access Control.
|
||||
<br />
|
||||
<br />A token will automatically be generated to be used with the CLI command.
|
||||
</>
|
||||
|
||||
@@ -41,10 +41,10 @@ const formSchemaWithIdentity = baseFormSchema.extend({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
},
|
||||
{ required_error: "Identity is required" }
|
||||
{ required_error: "Machine identity is required" }
|
||||
)
|
||||
.nullable()
|
||||
.refine((val) => val !== null, { message: "Identity is required" })
|
||||
.refine((val) => val !== null, { message: "Machine identity is required" })
|
||||
});
|
||||
|
||||
const formSchemaWithToken = baseFormSchema.extend({
|
||||
@@ -229,8 +229,8 @@ export const RelayCliDeploymentMethod = () => {
|
||||
{canCreateToken && autogenerateToken ? (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity"
|
||||
tooltipText="The identity that your relay will use for authentication."
|
||||
label="Machine Identity"
|
||||
tooltipText="The machine identity that your relay will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
@@ -244,7 +244,7 @@ export const RelayCliDeploymentMethod = () => {
|
||||
)
|
||||
}
|
||||
isLoading={isIdentitiesLoading}
|
||||
placeholder="Select identity..."
|
||||
placeholder="Select machine identity..."
|
||||
options={identityMembershipOrgs.map((membership) => membership.identity)}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
@@ -254,14 +254,14 @@ export const RelayCliDeploymentMethod = () => {
|
||||
) : (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity Token"
|
||||
tooltipText="The identity token that your relay will use for authentication."
|
||||
label="Machine Identity Token"
|
||||
tooltipText="The machine identity token that your relay will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={identityToken}
|
||||
onChange={(e) => setIdentityToken(e.target.value)}
|
||||
placeholder="Enter identity token..."
|
||||
placeholder="Enter machine identity token..."
|
||||
isError={Boolean(errors.identityToken)}
|
||||
/>
|
||||
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
|
||||
@@ -279,15 +279,15 @@ export const RelayCliDeploymentMethod = () => {
|
||||
className="mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>Automatically enable token auth and generate a token for identity</span>
|
||||
<span>Automatically enable token auth and generate a token for machine identity</span>
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
Token authentication will be automatically enabled for the selected identity if
|
||||
it isn't already configured. By default, it will be configured to allow all
|
||||
IP addresses with a token TTL of 30 days. You can manage these settings in
|
||||
Access Control.
|
||||
Token authentication will be automatically enabled for the selected machine
|
||||
identity if it isn't already configured. By default, it will be configured
|
||||
to allow all IP addresses with a token TTL of 30 days. You can manage these
|
||||
settings in Access Control.
|
||||
<br />
|
||||
<br />A token will automatically be generated to be used with the CLI command.
|
||||
</>
|
||||
|
||||
@@ -41,10 +41,10 @@ const formSchemaWithIdentity = baseFormSchema.extend({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
},
|
||||
{ required_error: "Identity is required" }
|
||||
{ required_error: "Machine identity is required" }
|
||||
)
|
||||
.nullable()
|
||||
.refine((val) => val !== null, { message: "Identity is required" })
|
||||
.refine((val) => val !== null, { message: "Machine identity is required" })
|
||||
});
|
||||
|
||||
const formSchemaWithToken = baseFormSchema.extend({
|
||||
@@ -270,8 +270,8 @@ export const RelayCliSystemdDeploymentMethod = () => {
|
||||
{canCreateToken && autogenerateToken ? (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity"
|
||||
tooltipText="The identity that your relay will use for authentication."
|
||||
label="Machine Identity"
|
||||
tooltipText="The machine identity that your relay will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
@@ -285,7 +285,7 @@ export const RelayCliSystemdDeploymentMethod = () => {
|
||||
)
|
||||
}
|
||||
isLoading={isIdentitiesLoading}
|
||||
placeholder="Select identity..."
|
||||
placeholder="Select machine identity..."
|
||||
options={identityMembershipOrgs.map((membership) => membership.identity)}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
@@ -295,14 +295,14 @@ export const RelayCliSystemdDeploymentMethod = () => {
|
||||
) : (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity Token"
|
||||
tooltipText="The identity token that your relay will use for authentication."
|
||||
label="Machine Identity Token"
|
||||
tooltipText="The machine identity token that your relay will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={identityToken}
|
||||
onChange={(e) => setIdentityToken(e.target.value)}
|
||||
placeholder="Enter identity token..."
|
||||
placeholder="Enter machine identity token..."
|
||||
isError={Boolean(errors.identityToken)}
|
||||
/>
|
||||
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
|
||||
@@ -320,15 +320,15 @@ export const RelayCliSystemdDeploymentMethod = () => {
|
||||
className="mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>Automatically enable token auth and generate a token for identity</span>
|
||||
<span>Automatically enable token auth and generate a token for machine identity</span>
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
Token authentication will be automatically enabled for the selected identity if
|
||||
it isn't already configured. By default, it will be configured to allow all
|
||||
IP addresses with a token TTL of 30 days. You can manage these settings in
|
||||
Access Control.
|
||||
Token authentication will be automatically enabled for the selected machine
|
||||
identity if it isn't already configured. By default, it will be configured
|
||||
to allow all IP addresses with a token TTL of 30 days. You can manage these
|
||||
settings in Access Control.
|
||||
<br />
|
||||
<br />A token will automatically be generated to be used with the CLI command.
|
||||
</>
|
||||
|
||||
@@ -42,10 +42,10 @@ const formSchemaWithIdentity = baseFormSchema.extend({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
},
|
||||
{ required_error: "Identity is required" }
|
||||
{ required_error: "Machine identity is required" }
|
||||
)
|
||||
.nullable()
|
||||
.refine((val) => val !== null, { message: "Identity is required" })
|
||||
.refine((val) => val !== null, { message: "Machine identity is required" })
|
||||
});
|
||||
|
||||
const formSchemaWithToken = baseFormSchema.extend({
|
||||
@@ -349,8 +349,8 @@ resource "aws_eip_association" "eip_assoc" {
|
||||
{canCreateToken && autogenerateToken ? (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity"
|
||||
tooltipText="The identity that your relay will use for authentication."
|
||||
label="Machine Identity"
|
||||
tooltipText="The machine identity that your relay will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<FilterableSelect
|
||||
@@ -364,7 +364,7 @@ resource "aws_eip_association" "eip_assoc" {
|
||||
)
|
||||
}
|
||||
isLoading={isIdentitiesLoading}
|
||||
placeholder="Select identity..."
|
||||
placeholder="Select machine identity..."
|
||||
options={identityMembershipOrgs.map((membership) => membership.identity)}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
@@ -374,14 +374,14 @@ resource "aws_eip_association" "eip_assoc" {
|
||||
) : (
|
||||
<>
|
||||
<FormLabel
|
||||
label="Identity Token"
|
||||
tooltipText="The identity token that your relay will use for authentication."
|
||||
label="Machine Identity Token"
|
||||
tooltipText="The machine identity token that your relay will use for authentication."
|
||||
className="mt-4"
|
||||
/>
|
||||
<Input
|
||||
value={identityToken}
|
||||
onChange={(e) => setIdentityToken(e.target.value)}
|
||||
placeholder="Enter identity token..."
|
||||
placeholder="Enter machine identity token..."
|
||||
isError={Boolean(errors.identityToken)}
|
||||
/>
|
||||
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
|
||||
@@ -399,15 +399,15 @@ resource "aws_eip_association" "eip_assoc" {
|
||||
className="mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>Automatically enable token auth and generate a token for identity</span>
|
||||
<span>Automatically enable token auth and generate a token for machine identity</span>
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
Token authentication will be automatically enabled for the selected identity if
|
||||
it isn't already configured. By default, it will be configured to allow all
|
||||
IP addresses with a token TTL of 30 days. You can manage these settings in
|
||||
Access Control.
|
||||
Token authentication will be automatically enabled for the selected machine
|
||||
identity if it isn't already configured. By default, it will be configured
|
||||
to allow all IP addresses with a token TTL of 30 days. You can manage these
|
||||
settings in Access Control.
|
||||
<br />
|
||||
<br />A token will automatically be generated to be used with the CLI command.
|
||||
</>
|
||||
|
||||
@@ -44,7 +44,7 @@ const Page = () => {
|
||||
<PageHeader
|
||||
scope={currentProject.type}
|
||||
title="Access Control"
|
||||
description="Manage fine-grained access for users, groups, roles, and identities within your project resources."
|
||||
description="Manage fine-grained access for users, groups, roles, and machine identities within your project resources."
|
||||
/>
|
||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
|
||||
<TabList>
|
||||
@@ -55,7 +55,7 @@ const Page = () => {
|
||||
Groups
|
||||
</Tab>
|
||||
<Tab variant="project" value={ProjectAccessControlTabs.Identities}>
|
||||
Identities
|
||||
Machine Identities
|
||||
</Tab>
|
||||
{isSecretManager && (
|
||||
<Tab variant="project" value={ProjectAccessControlTabs.ServiceTokens}>
|
||||
|
||||
@@ -158,7 +158,7 @@ export const IdentityTab = withProjectPermission(
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted project identity",
|
||||
text: "Successfully deleted project machine identity",
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
@@ -168,7 +168,7 @@ export const IdentityTab = withProjectPermission(
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully removed identity from project",
|
||||
text: "Successfully removed machine identity from project",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
@@ -197,7 +197,7 @@ export const IdentityTab = withProjectPermission(
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
|
||||
<p className="text-xl font-medium text-mineshaft-100">Machine Identities</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
@@ -212,7 +212,7 @@ export const IdentityTab = withProjectPermission(
|
||||
onClick={() => handlePopUpOpen("createIdentity")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create Identity
|
||||
Add Machine Identity
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@@ -223,7 +223,7 @@ export const IdentityTab = withProjectPermission(
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search identities by name..."
|
||||
placeholder="Search machine identities by name..."
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
@@ -454,7 +454,9 @@ export const IdentityTab = withProjectPermission(
|
||||
});
|
||||
}}
|
||||
>
|
||||
{identityProjectId ? "Delete Identity" : "Remove From Project"}
|
||||
{identityProjectId
|
||||
? "Delete Machine Identity"
|
||||
: "Remove From Project"}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@@ -474,7 +476,7 @@ export const IdentityTab = withProjectPermission(
|
||||
<Td colSpan={3}>
|
||||
<Blur
|
||||
className="w-min"
|
||||
tooltipText="You do not have permission to read this identity."
|
||||
tooltipText="You do not have permission to view this machine identity."
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
@@ -497,8 +499,8 @@ export const IdentityTab = withProjectPermission(
|
||||
<EmptyState
|
||||
title={
|
||||
debouncedSearch.trim().length > 0
|
||||
? "No identities match search filter"
|
||||
: "No identities have been added to this project"
|
||||
? "No machine identities match search filter"
|
||||
: "No machine identities have been added to this project"
|
||||
}
|
||||
icon={faServer}
|
||||
/>
|
||||
@@ -513,8 +515,8 @@ export const IdentityTab = withProjectPermission(
|
||||
>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
title="Add Project Identity"
|
||||
subTitle="Create a new identity or assign an existing identity"
|
||||
title="Add Project Machine Identity"
|
||||
subTitle="Create a new machine identity or assign an existing one"
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{wizardStep === WizardSteps.SelectAction && (
|
||||
@@ -538,11 +540,11 @@ export const IdentityTab = withProjectPermission(
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<PlusIcon size="1rem" />
|
||||
<div>Create New Identity</div>
|
||||
<div>Create Machine Identity</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-mineshaft-300">
|
||||
Create a new machine identity specifically for this project. This identity
|
||||
will be managed at the project-level.
|
||||
Create a new machine identity specifically for this project. This machine
|
||||
identity will be managed at the project-level.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -558,11 +560,11 @@ export const IdentityTab = withProjectPermission(
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkIcon size="1rem" />
|
||||
<div>Assign Existing Identity</div>
|
||||
<div>Assign Existing Machine Identity</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-mineshaft-300">
|
||||
Assign an existing identity from your organization. The identity will continue
|
||||
to be managed at its original scope.
|
||||
Assign an existing machine identity from your organization. The machine
|
||||
identity will continue to be managed at its original scope.
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -138,7 +138,7 @@ export const ProjectIdentityModal = ({ onClose, identity }: ContentProps) => {
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isUpdate ? "updated" : "created"} project identity`,
|
||||
text: `Successfully ${isUpdate ? "updated" : "created"} project machine identity`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -148,7 +148,7 @@ export const ProjectIdentityModal = ({ onClose, identity }: ContentProps) => {
|
||||
const error = err as any;
|
||||
const text =
|
||||
error?.response?.data?.message ??
|
||||
`Failed to ${isUpdate ? "update" : "create"} project identity`;
|
||||
`Failed to ${isUpdate ? "update" : "create"} project machine identity`;
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
|
||||
@@ -83,7 +83,7 @@ export const ProjectLinkIdentityModal = ({ handlePopUpToggle }: Props) => {
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully added identity to project",
|
||||
text: "Successfully added machine identity to project",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -114,11 +114,11 @@ export const ProjectLinkIdentityModal = ({ handlePopUpToggle }: Props) => {
|
||||
control={control}
|
||||
name="identity"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||
<FormControl label="Machine Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select identity..."
|
||||
placeholder="Select machine identity..."
|
||||
// onInputChange={setSearchValue}
|
||||
options={filteredIdentityMembershipOrgs.map((membership) => ({
|
||||
name: membership.name,
|
||||
|
||||
@@ -92,7 +92,7 @@ const Page = () => {
|
||||
onSuccess: () => {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Identity privilege assumption has started"
|
||||
text: "Machine identity privilege assumption has started"
|
||||
});
|
||||
const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization && isNonScopedIdentity ? `?subOrganization=${currentOrg.slug}` : ""}`;
|
||||
window.location.assign(
|
||||
@@ -109,7 +109,7 @@ const Page = () => {
|
||||
projectId
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully removed identity from project",
|
||||
text: "Successfully removed machine identity from project",
|
||||
type: "success"
|
||||
});
|
||||
handlePopUpClose("deleteIdentity");
|
||||
@@ -149,12 +149,12 @@ const Page = () => {
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Identities
|
||||
Machine Identities
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={currentProject.type}
|
||||
title={identityMembershipDetails?.identity?.name}
|
||||
description={`Identity ${isProjectIdentity ? "created" : "added"} on ${identityMembershipDetails?.createdAt && formatRelative(new Date(identityMembershipDetails?.createdAt || ""), new Date())}`}
|
||||
description={`Machine identity ${isProjectIdentity ? "created" : "added"} on ${identityMembershipDetails?.createdAt && formatRelative(new Date(identityMembershipDetails?.createdAt || ""), new Date())}`}
|
||||
className={!isProjectIdentity ? "mb-4" : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -177,7 +177,7 @@ const Page = () => {
|
||||
identityId: identityMembershipDetails?.identity.id
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Assume privileges of the user"
|
||||
allowedLabel="Assume privileges of the machine identity"
|
||||
passThrough={false}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
@@ -209,7 +209,7 @@ const Page = () => {
|
||||
isLoading={isDeletingIdentity}
|
||||
onClick={() => handlePopUpOpen("deleteIdentity")}
|
||||
>
|
||||
Remove Identity
|
||||
Remove Machine Identity
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@@ -219,7 +219,7 @@ const Page = () => {
|
||||
{!isProjectIdentity && (
|
||||
<Alert hideTitle iconClassName="text-info" className="mb-4 border-info/50 bg-info/10">
|
||||
<AlertDescription>
|
||||
This identity is managed by your organization.{" "}
|
||||
This machine identity is managed by your organization.{" "}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Read}
|
||||
an={OrgPermissionSubjects.Identity}
|
||||
@@ -234,7 +234,7 @@ const Page = () => {
|
||||
}}
|
||||
>
|
||||
<span className="cursor-pointer text-info underline underline-offset-2">
|
||||
Click here to manage identity.
|
||||
Click here to manage machine identity.
|
||||
</span>
|
||||
</Link>
|
||||
) : null
|
||||
@@ -286,15 +286,15 @@ const Page = () => {
|
||||
<ConfirmActionModal
|
||||
isOpen={popUp.assumePrivileges.isOpen}
|
||||
confirmKey="assume"
|
||||
title="Do you want to assume privileges of this identity?"
|
||||
subTitle="This will set your privileges to those of the identity for the next hour."
|
||||
title="Do you want to assume privileges of this machine identity?"
|
||||
subTitle="This will set your privileges to those of the machine identity for the next hour."
|
||||
onChange={(isOpen) => handlePopUpToggle("assumePrivileges", isOpen)}
|
||||
onConfirmed={handleAssumePrivileges}
|
||||
buttonText="Confirm"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState title="Error: Unable to find the identity." className="py-12" />
|
||||
<EmptyState title="Error: Unable to find the machine identity." className="py-12" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -235,7 +235,10 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && !identityProjectPrivileges?.length && (
|
||||
<EmptyState title="This identity has no additional privileges" icon={faFolder} />
|
||||
<EmptyState
|
||||
title="This machine identity has no additional privileges"
|
||||
icon={faFolder}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete project identity"
|
||||
text: "Failed to delete project machine identity"
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -77,7 +77,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Identity Details</h3>
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Details</h3>
|
||||
<DropdownMenu>
|
||||
{!isOrgIdentity && (
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -114,7 +114,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Edit Identity
|
||||
Edit Machine Identity
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@@ -137,7 +137,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Identity
|
||||
Delete Machine Identity
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@@ -146,7 +146,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Identity ID</p>
|
||||
<p className="text-sm font-medium text-mineshaft-300">Machine Identity ID</p>
|
||||
<div className="group flex align-top">
|
||||
<p className="text-sm break-all text-mineshaft-300">{identity.id}</p>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
faArrowRight,
|
||||
faArrowRightToBracket,
|
||||
faArrowUp,
|
||||
faCheck,
|
||||
faCopy,
|
||||
faFilter,
|
||||
faFingerprint,
|
||||
faFolder,
|
||||
@@ -79,6 +81,7 @@ import {
|
||||
usePopUp,
|
||||
useResetPageHelper,
|
||||
useResizableHeaderHeight,
|
||||
useTimedReset,
|
||||
useToggle
|
||||
} from "@app/hooks";
|
||||
import {
|
||||
@@ -194,6 +197,15 @@ export const OverviewPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const [copiedSlug, , setCopiedSlug] = useTimedReset<string>({
|
||||
initialState: ""
|
||||
});
|
||||
|
||||
const copyToClipboard = (value: string, slug: string) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
setCopiedSlug(slug);
|
||||
};
|
||||
|
||||
const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE);
|
||||
const [filterHistory, setFilterHistory] = useState<
|
||||
Map<string, { filter: Filter; searchFilter: string }>
|
||||
@@ -1324,19 +1336,33 @@ export const OverviewPage = () => {
|
||||
>
|
||||
<Tooltip
|
||||
content={
|
||||
collapseEnvironments ? (
|
||||
<p className="whitespace-break-spaces">{name}</p>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
<div className="flex flex-col gap-2">
|
||||
{collapseEnvironments ? (
|
||||
<p className="whitespace-break-spaces text-mineshaft-300">{name}</p>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs text-mineshaft-300">{slug}</p>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="Copy environment slug"
|
||||
onClick={() => copyToClipboard(slug, slug)}
|
||||
>
|
||||
<FontAwesomeIcon icon={copiedSlug === slug ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
side="bottom"
|
||||
sideOffset={-1}
|
||||
align="end"
|
||||
sideOffset={5}
|
||||
align="center"
|
||||
className="max-w-xl text-xs normal-case"
|
||||
rootProps={{
|
||||
disableHoverableContent: true
|
||||
disableHoverableContent: false
|
||||
}}
|
||||
key={`tooltip-${name}-${index + 1}`}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -31,7 +32,13 @@ type ContentProps = {
|
||||
const Content = ({ onComplete }: ContentProps) => {
|
||||
const { currentProject } = useProject();
|
||||
const { mutateAsync, isPending } = useCreateWsEnvironment();
|
||||
const { control, handleSubmit } = useForm<FormData>({
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { dirtyFields }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
@@ -52,15 +59,28 @@ const Content = ({ onComplete }: ContentProps) => {
|
||||
onComplete(env);
|
||||
};
|
||||
|
||||
const handleEnvironmentNameChange = () => {
|
||||
if (dirtyFields.environmentSlug) return;
|
||||
|
||||
const value = getValues("environmentName");
|
||||
setValue("environmentSlug", slugify(value, { lowercase: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="environmentName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Environment Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
<Input
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
onChange(e);
|
||||
handleEnvironmentNameChange();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user