diff --git a/.env.example b/.env.example index f67488c23..9da28e528 100644 --- a/.env.example +++ b/.env.example @@ -31,25 +31,14 @@ SMTP_FROM_NAME= SMTP_USERNAME= SMTP_PASSWORD= -# Integration -# Optional only if integration is used -CLIENT_ID_HEROKU= -CLIENT_ID_VERCEL= -CLIENT_ID_NETLIFY= +# CICD Integration CLIENT_ID_GITHUB= CLIENT_ID_GITHUB_APP= CLIENT_SLUG_GITHUB_APP= -CLIENT_ID_GITLAB= -CLIENT_ID_BITBUCKET= -CLIENT_SECRET_HEROKU= -CLIENT_SECRET_VERCEL= -CLIENT_SECRET_NETLIFY= CLIENT_SECRET_GITHUB= CLIENT_SECRET_GITHUB_APP= +CLIENT_ID_GITLAB= CLIENT_SECRET_GITLAB= -CLIENT_SECRET_BITBUCKET= -CLIENT_SLUG_VERCEL= - CLIENT_PRIVATE_KEY_GITHUB_APP= CLIENT_APP_ID_GITHUB_APP= diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a8a64e7b4..2803cbbb5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,23 +1,25 @@ -# Description 📣 +## Context - + -## Type ✨ +## Screenshots -- [ ] Bug fix -- [ ] New feature + + +## Steps to verify the change + +## Type + +- [ ] Fix +- [ ] Feature - [ ] Improvement -- [ ] Breaking change -- [ ] Documentation +- [ ] Breaking +- [ ] Docs +- [ ] Chore -# Tests 🛠️ +## Checklist - - -```sh -# Here's some code block to paste some code snippets -``` - ---- - -- [ ] I have read the [contributing guide](https://infisical.com/docs/contributing/getting-started/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/getting-started/code-of-conduct). 📝 \ No newline at end of file +- [ ] Title follows the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) format: `type(scope): short description` (scope is optional, e.g., `fix: prevent crash on sync` or `fix(api): handle null response`). +- [ ] Tested locally +- [ ] Updated docs (if needed) +- [ ] Read the [contributing guide](https://infisical.com/docs/contributing/getting-started/overview) \ No newline at end of file diff --git a/.github/workflows/validate-pr-title.yml b/.github/workflows/validate-pr-title.yml new file mode 100644 index 000000000..1e590139c --- /dev/null +++ b/.github/workflows/validate-pr-title.yml @@ -0,0 +1,55 @@ +name: Validate PR Title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +jobs: + validate-pr-title: + name: Validate PR Title Format + runs-on: ubuntu-latest + steps: + - name: Check PR Title Format + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const title = context.payload.pull_request.title; + + // Valid PR types based on pull_request_template.md + const validTypes = ['fix', 'feature', 'improvement', 'breaking', 'docs', 'chore']; + + // Regex pattern: type(optional-scope): short description + // - Type must be one of the valid types + // - Scope is optional, must be in parentheses, lowercase alphanumeric with hyphens + // - Followed by colon, space, and description (must start with lowercase letter) + const pattern = new RegExp(`^(${validTypes.join('|')})(\\([a-z0-9-]+\\))?: [a-z].+$`); + + if (!pattern.test(title)) { + const errorMessage = ` + ❌ **Invalid PR Title Format** + + Your PR title: \`${title}\` + + **Expected format:** \`type(scope): short description\` (description must start with lowercase) + + **Valid types:** + - \`fix\` - Bug fixes + - \`feature\` - New features + - \`improvement\` - Enhancements to existing features + - \`breaking\` - Breaking changes + - \`docs\` - Documentation updates + - \`chore\` - Maintenance tasks + + **Scope:** Optional, short identifier in parentheses (e.g., \`(api)\`, \`(auth)\`, \`(ui)\`) + + **Examples:** + - \`fix: prevent crash on sync\` + - \`fix(api): handle null response from auth endpoint\` + - \`docs(cli): update installation guide\` + `; + + core.setFailed(errorMessage); + } else { + console.log(`✅ PR title is valid: "${title}"`); + } + diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 01c9a737b..0674a00e9 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -34,6 +34,7 @@ ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY ARG INTERCOM_ID ENV VITE_INTERCOM_ID $INTERCOM_ID ARG INFISICAL_PLATFORM_VERSION +ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ARG CAPTCHA_SITE_KEY ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY diff --git a/backend/bdd/features/environment.py b/backend/bdd/features/environment.py index 976998c72..52fda3eca 100644 --- a/backend/bdd/features/environment.py +++ b/backend/bdd/features/environment.py @@ -87,14 +87,13 @@ def bootstrap_infisical(context: Context): ca_slug = faker.slug() resp = client.post( - "/api/v1/pki/ca/internal", + "/api/v1/cert-manager/ca/internal", headers=headers, json={ "projectId": project["id"], "name": ca_slug, "type": "internal", "status": "active", - "enableDirectIssuance": True, "configuration": { "type": "root", "organization": "Infisican Inc", @@ -115,7 +114,7 @@ def bootstrap_infisical(context: Context): cert_template_slug = faker.slug() resp = client.post( - "/api/v2/certificate-templates", + "/api/v1/cert-manager/certificate-templates", headers=headers, json={ "projectId": project["id"], diff --git a/backend/bdd/features/pki/acme/access-control.feature b/backend/bdd/features/pki/acme/access-control.feature index 50588be76..053127077 100644 --- a/backend/bdd/features/pki/acme/access-control.feature +++ b/backend/bdd/features/pki/acme/access-control.feature @@ -2,7 +2,7 @@ Feature: Access Control Scenario Outline: Access resources across different account Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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_account0 Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id When I create certificate signing request as csr @@ -34,7 +34,7 @@ Feature: Access Control Then the value response.status_code should not be equal to 404 And I put away current ACME client as client0 - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email maidu@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account1 Then I peak and memorize the next nonce as nonce When I send a raw ACME request to "" @@ -53,7 +53,7 @@ Feature: Access Control Examples: Endpoints | src_var | jq | dest_var | url | payload | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | | | order | . | not_used | {order.uri} | | | order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} | | order | . | not_used | {order.uri}/certificate | | @@ -62,7 +62,7 @@ Feature: Access Control Scenario Outline: Access resources across a different profiles Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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_account0 Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id When I create certificate signing request as csr @@ -96,7 +96,7 @@ Feature: Access Control Given I make a random slug as profile_slug Given I use AUTH_TOKEN for authentication - When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload + When I send a "POST" request to "/api/v1/cert-manager/certificate-profiles" with JSON payload """ { "projectId": "{PROJECT_ID}", @@ -110,10 +110,10 @@ Feature: Access Control """ Then the value response.status_code should be equal to 200 Then I memorize response with jq ".certificateProfile.id" as profile_id - When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" + When I send a "GET" request to "/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal" Then I memorize response with jq ".eabKid" as eab_kid And I memorize response with jq ".eabSecret" as eab_secret - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{profile_id}/directory" Then I register a new ACME account with email maidu@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account1 Then I peak and memorize the next nonce as nonce Then I memorize with jq "" as @@ -133,7 +133,7 @@ Feature: Access Control Examples: Endpoints | src_var | jq | dest_var | url | payload | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | | | order | . | not_used | {order.uri} | | | order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} | | order | . | not_used | {order.uri}/certificate | | @@ -143,7 +143,7 @@ Feature: Access Control Scenario Outline: Access resources across a different profile with the same key pair Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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_account0 Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id When I create certificate signing request as csr @@ -177,7 +177,7 @@ Feature: Access Control Given I make a random slug as profile_slug Given I use AUTH_TOKEN for authentication - When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload + When I send a "POST" request to "/api/v1/cert-manager/certificate-profiles" with JSON payload """ { "projectId": "{PROJECT_ID}", @@ -191,10 +191,10 @@ Feature: Access Control """ Then the value response.status_code should be equal to 200 Then I memorize response with jq ".certificateProfile.id" as profile_id - When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" + When I send a "GET" request to "/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal" Then I memorize response with jq ".eabKid" as eab_kid And I memorize response with jq ".eabSecret" as eab_secret - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory" with the key pair from client0 + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{profile_id}/directory" with the key pair from client0 Then I register a new ACME account with email maidu@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account1 Then I peak and memorize the next nonce as nonce Then I memorize with jq "" as @@ -214,7 +214,7 @@ Feature: Access Control Examples: Endpoints | src_var | jq | dest_var | url | payload | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | | | order | . | not_used | {order.uri} | | | order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} | | order | . | not_used | {order.uri}/certificate | | @@ -223,7 +223,7 @@ Feature: Access Control Scenario Outline: URL mismatch Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 Then I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id When I create certificate signing request as csr @@ -258,8 +258,8 @@ Feature: Access Control Examples: Endpoints | src_var | jq | dest_var | actual_url | bad_url | error_detail | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | BAD | Invalid URL in the protected header | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | https://evil.com/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | URL mismatch in the protected header | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | BAD | Invalid URL in the protected header | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | https://evil.com/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | URL mismatch in the protected header | | order | . | not_used | {order.uri} | BAD | Invalid URL in the protected header | | order | . | not_used | {order.uri} | https://example.com/acmes/orders/FOOBAR | URL mismatch in the protected header | | order | . | not_used | {order.uri}/finalize | BAD | Invalid URL in the protected header | @@ -273,7 +273,7 @@ Feature: Access Control Scenario Outline: Send KID and JWK in the same time Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 And I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id When I create certificate signing request as csr @@ -312,8 +312,8 @@ Feature: Access Control Examples: Endpoints | src_var | jq | dest_var | url | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order | | order | . | not_used | {order.uri} | | order | . | not_used | {order.uri}/finalize | | order | . | not_used | {order.uri}/certificate | diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 14e304c6c..c7eb25a53 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -2,13 +2,13 @@ Feature: Account Scenario: Create a new account Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 - And the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/(.+) + And the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/(.+) Scenario: Create a new account with the same key pair twice Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 And I memorize acme_account.uri as kid And 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_account2 @@ -17,7 +17,7 @@ Feature: Account Scenario: Find an existing account Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 And I memorize acme_account.uri as account_uri And I find the existing ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as retrieved_account @@ -26,7 +26,7 @@ Feature: Account # Note: This is a very special case for cert-manager. Scenario: Create a new account with EAB then retrieve it without EAB Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 And I memorize acme_account.uri as account_uri And I find the existing ACME account without EAB as retrieved_account @@ -35,13 +35,13 @@ Feature: Account Scenario: Create a new account without EAB Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com without EAB And the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" Scenario Outline: Scenario: Create a new account with bad EAB credentials Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "" with secret "" as acme_account And the value error with jq ".type" should be equal to "" And the value error with jq ".detail" should be equal to "" @@ -57,17 +57,17 @@ Feature: Account Scenario Outline: Scenario: Create a new account with bad EAB url Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory" And I use a different new-account URL "" for EAB signature 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 And the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" And the value error with jq ".detail" should be equal to "External account binding URL mismatch" Examples: Bad URLs - | url | - | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account-bad | - | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account?foo=bar | - | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account#foobar | - | {BASE_URL}/acme/new-account | - | https://example.com/api/v1/pki/acme/profiles/{acme_profile.id}/new-account-bad | - | bad | + | url | + | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account-bad | + | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account?foo=bar | + | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account#foobar | + | {BASE_URL}/acme/new-account | + | https://example.com/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account-bad | + | bad | diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index 46cc9d4e2..757a182c8 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -2,7 +2,7 @@ Feature: Authorization Scenario: Get authorization Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 @@ -14,7 +14,7 @@ Feature: Authorization Then 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 the value order.authorizations[0].uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) + And the value order.authorizations[0].uri with jq "." should match pattern {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/authorizations/(.+) And the value order.authorizations[0].body with jq ".status" should be equal to "pending" And the value order.authorizations[0].body with jq ".challenges | map(pick(.type, .status)) | sort_by(.type)" should be equal to json """ diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 3c292e8ba..4c3b84ab9 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -3,7 +3,7 @@ Feature: ACME Cert Profile Scenario: Create a cert profile Given I make a random slug as profile_slug And I use AUTH_TOKEN for authentication - When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload + When I send a "POST" request to "/api/v1/cert-manager/certificate-profiles" with JSON payload """ { "projectId": "{PROJECT_ID}", @@ -25,7 +25,7 @@ Feature: ACME Cert Profile Scenario: Reveal EAB secret Given I make a random slug as profile_slug And I use AUTH_TOKEN for authentication - When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload + When I send a "POST" request to "/api/v1/cert-manager/certificate-profiles" with JSON payload """ { "projectId": "{PROJECT_ID}", @@ -39,11 +39,11 @@ Feature: ACME Cert Profile """ Then the value response.status_code should be equal to 200 And I memorize response with jq ".certificateProfile.id" as profile_id - When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" + When I send a "GET" request to "/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal" Then the value response.status_code should be equal to 200 And the value response with jq ".eabKid" should be equal to "{profile_id}" And the value response with jq ".eabSecret" should be present And I memorize response with jq ".eabKid" as eab_kid And I memorize response with jq ".eabSecret" as eab_secret - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{profile_id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index 67f73aab2..21c63329f 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -2,7 +2,7 @@ Feature: Challenge Scenario: Validate challenge Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 @@ -24,7 +24,7 @@ Feature: Challenge Scenario: Validate challenges for multiple domains Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 @@ -58,7 +58,7 @@ Feature: Challenge Scenario: Did not finish all challenges Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 @@ -153,7 +153,7 @@ Feature: Challenge Scenario: CSR names mismatch with order identifier Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 @@ -165,13 +165,13 @@ Feature: Challenge 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 Then I peak and memorize the next nonce as nonce - When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" + When I send a raw ACME request to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order" """ { "protected": { "alg": "RS256", "nonce": "{nonce}", - "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order", + "url": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order", "kid": "{acme_account.uri}" }, "payload": { diff --git a/backend/bdd/features/pki/acme/directory.feature b/backend/bdd/features/pki/acme/directory.feature index 53084a681..30a94af38 100644 --- a/backend/bdd/features/pki/acme/directory.feature +++ b/backend/bdd/features/pki/acme/directory.feature @@ -2,14 +2,14 @@ Feature: Directory Scenario: Get the directory of ACME service urls Given I have an ACME cert profile as "acme_profile" - When I send a "GET" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I send a "GET" request to "/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory" Then the response status code should be "200" And the response body should match JSON value """ { - "newNonce": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce", - "newAccount": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account", - "newOrder": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order", + "newNonce": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-nonce", + "newAccount": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account", + "newOrder": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order", "meta": { "externalAccountRequired": true } diff --git a/backend/bdd/features/pki/acme/external-ca.feature b/backend/bdd/features/pki/acme/external-ca.feature index 26bfd84ad..2a900dd10 100644 --- a/backend/bdd/features/pki/acme/external-ca.feature +++ b/backend/bdd/features/pki/acme/external-ca.feature @@ -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 @@ -87,14 +88,12 @@ Feature: External CA """ 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" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 """ - { - "COMMON_NAME": "localhost" - } + """ # 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" ] - """ \ No newline at end of file + """ + + 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/cert-manager/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 + """ + + """ + # 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"} | + | {} | diff --git a/backend/bdd/features/pki/acme/internal-ca.feature b/backend/bdd/features/pki/acme/internal-ca.feature index 9db4328d2..934b7bef3 100644 --- a/backend/bdd/features/pki/acme/internal-ca.feature +++ b/backend/bdd/features/pki/acme/internal-ca.feature @@ -2,7 +2,7 @@ Feature: Internal CA Scenario: CSR with SANs only Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index 9a55ae284..93fc3f981 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -2,13 +2,13 @@ Feature: Nonce Scenario: Generate a new nonce Given I have an ACME cert profile as "acme_profile" - When I send a "HEAD" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce" + When I send a "HEAD" request to "/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-nonce" Then the response status code should be "200" And the response header "Replay-Nonce" should contains non-empty value Scenario Outline: Send a bad nonce to account endpoints Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 And I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id When I create certificate signing request as csr @@ -40,18 +40,18 @@ Feature: Nonce And the value response with jq ".detail" should be equal to "Invalid nonce" Examples: Endpoints - | src_var | jq | dest_var | url | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | - | order | . | not_used | {order.uri} | - | order | . | not_used | {order.uri}/finalize | - | order | . | not_used | {order.uri}/certificate | - | order | .authorizations[0].uri | auth_uri | {auth_uri} | - | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | + | src_var | jq | dest_var | url | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order | + | order | . | not_used | {order.uri} | + | order | . | not_used | {order.uri}/finalize | + | order | . | not_used | {order.uri}/certificate | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | Scenario Outline: Send the same nonce twice Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 And I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id When I create certificate signing request as csr @@ -65,13 +65,13 @@ Feature: Nonce 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 peak and memorize the next nonce as nonce_value - When I send a raw ACME request to "/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders" + When I send a raw ACME request to "/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders" """ { "protected": { "alg": "RS256", "nonce": "{nonce_value}", - "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders", + "url": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders", "kid": "{acme_account.uri}" }, "payload": {} @@ -97,11 +97,11 @@ Feature: Nonce And the value response with jq ".detail" should be equal to "Invalid nonce" Examples: Endpoints - | src_var | jq | dest_var | url | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | - | order | . | not_used | {order.uri} | - | order | . | not_used | {order.uri}/finalize | - | order | . | not_used | {order.uri}/certificate | - | order | .authorizations[0].uri | auth_uri | {auth_uri} | - | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | + | src_var | jq | dest_var | url | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | + | order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order | + | order | . | not_used | {order.uri} | + | order | . | not_used | {order.uri}/finalize | + | order | . | not_used | {order.uri}/certificate | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index 19f467f00..199cd4aa6 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -2,7 +2,7 @@ Feature: Order Scenario: Create a new order Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 @@ -14,15 +14,15 @@ Feature: Order Then 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 the value order.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+) + And the value order.uri with jq "." should match pattern {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/orders/(.+) And the value order.body with jq ".status" should be equal to "pending" And the value order.body with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] - And the value order.body with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize - And the value order.body with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true + And the value order.body with jq ".finalize" should match pattern {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/orders/(.+)/finalize + And the value order.body with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true Scenario: Create a new order with SANs Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 @@ -52,7 +52,7 @@ Feature: Order Scenario: Fetch an order Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 @@ -67,21 +67,21 @@ Feature: Order And I send an ACME post-as-get to order.uri as fetched_order And the value fetched_order with jq ".status" should be equal to "pending" And the value fetched_order with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] - And the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize - And the value fetched_order with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true + And the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/orders/(.+)/finalize + And the value fetched_order with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true Scenario Outline: Create an order with invalid identifier types Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 And I peak and memorize the next nonce as nonce - When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" + When I send a raw ACME request to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order" """ { "protected": { "alg": "RS256", "nonce": "{nonce}", - "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order", + "url": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order", "kid": "{acme_account.uri}" }, "payload": { @@ -105,16 +105,16 @@ Feature: Order Scenario Outline: Create an order with invalid identifier values Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/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 And I peak and memorize the next nonce as nonce - When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" + When I send a raw ACME request to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order" """ { "protected": { "alg": "RS256", "nonce": "{nonce}", - "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order", + "url": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order", "kid": "{acme_account.uri}" }, "payload": { diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 353ec942d..e895ed69e 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -56,7 +56,7 @@ def step_impl(context: Context, profile_var: str): profile_slug = faker.slug() jwt_token = context.vars["AUTH_TOKEN"] response = context.http_client.post( - "/api/v1/pki/certificate-profiles", + "/api/v1/cert-manager/certificate-profiles", headers=dict(authorization="Bearer {}".format(jwt_token)), json={ "projectId": context.vars["PROJECT_ID"], @@ -74,7 +74,7 @@ def step_impl(context: Context, profile_var: str): kid = profile_id response = context.http_client.get( - f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal", + f"/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal", headers=dict(authorization="Bearer {}".format(jwt_token)), ) response.raise_for_status() @@ -147,13 +147,47 @@ 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"] ca_slug = faker.slug() config = replace_vars(json.loads(context.text), context.vars) response = context.http_client.post( - "/api/v1/pki/ca/acme", + "/api/v1/cert-manager/ca/acme", headers=dict(authorization="Bearer {}".format(jwt_token)), json={ "projectId": context.vars["PROJECT_ID"], @@ -174,7 +208,7 @@ def step_impl(context: Context, var_name: str): template_slug = faker.slug() config = replace_vars(json.loads(context.text), context.vars) response = context.http_client.post( - "/api/v2/certificate-templates", + "/api/v1/cert-manager/certificate-templates", headers=dict(authorization="Bearer {}".format(jwt_token)), json={ "projectId": context.vars["PROJECT_ID"], @@ -194,7 +228,7 @@ def step_impl(context: Context, ca_id: str, template_id: str, profile_var: str): profile_slug = faker.slug() jwt_token = context.vars["AUTH_TOKEN"] response = context.http_client.post( - "/api/v1/pki/certificate-profiles", + "/api/v1/cert-manager/certificate-profiles", headers=dict(authorization="Bearer {}".format(jwt_token)), json={ "projectId": context.vars["PROJECT_ID"], @@ -212,7 +246,7 @@ def step_impl(context: Context, ca_id: str, template_id: str, profile_var: str): kid = profile_id response = context.http_client.get( - f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal", + f"/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal", headers=dict(authorization="Bearer {}".format(jwt_token)), ) response.raise_for_status() @@ -236,7 +270,7 @@ def step_impl(context: Context, profile_var: str): profile_slug = faker.slug() jwt_token = context.vars["AUTH_TOKEN"] response = context.http_client.post( - "/api/v1/pki/certificate-profiles", + "/api/v1/cert-manager/certificate-profiles", headers=dict(authorization="Bearer {}".format(jwt_token)), json={ "projectId": context.vars["PROJECT_ID"], @@ -254,7 +288,7 @@ def step_impl(context: Context, profile_var: str): kid = profile_id response = context.http_client.get( - f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal", + f"/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal", headers=dict(authorization="Bearer {}".format(jwt_token)), ) response.raise_for_status() diff --git a/backend/src/db/migrations/20251126143442_add-notification-flag-scim-token.ts b/backend/src/db/migrations/20251126143442_add-notification-flag-scim-token.ts new file mode 100644 index 000000000..00dcf7902 --- /dev/null +++ b/backend/src/db/migrations/20251126143442_add-notification-flag-scim-token.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.ScimToken, "expiryNotificationSent"); + if (!hasCol) { + await knex.schema.alterTable(TableName.ScimToken, (t) => { + t.boolean("expiryNotificationSent").defaultTo(false); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasCol = await knex.schema.hasColumn(TableName.ScimToken, "expiryNotificationSent"); + if (hasCol) { + await knex.schema.alterTable(TableName.ScimToken, (t) => { + t.dropColumn("expiryNotificationSent"); + }); + } +} diff --git a/backend/src/db/schemas/scim-tokens.ts b/backend/src/db/schemas/scim-tokens.ts index ab6e10d27..6774b6bfd 100644 --- a/backend/src/db/schemas/scim-tokens.ts +++ b/backend/src/db/schemas/scim-tokens.ts @@ -13,7 +13,8 @@ export const ScimTokensSchema = z.object({ description: z.string(), orgId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + expiryNotificationSent: z.boolean().default(false).nullable().optional() }); export type TScimTokens = z.infer; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 7ff9ec09a..367c2833c 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -110,7 +110,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" }); await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" }); }, - { prefix: "/pki" } + { prefix: "/cert-manager" } ); await server.register( diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index c4ccf6be5..a73f955ae 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -77,7 +77,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { done(error, undefined); } }); - // GET /api/v1/pki/acme/profiles//directory + + // GET /api/v1/cert-manager/acme/profiles//directory // Directory (RFC 8555 Section 7.1.1) server.route({ method: "GET", @@ -99,7 +100,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { handler: async (req) => server.services.pkiAcme.getAcmeDirectory(req.params.profileId) }); - // HEAD /api/v1/pki/acme/profiles//new-nonce + // HEAD /api/v1/cert-manager/acme/profiles//new-nonce // New Nonce (RFC 8555 Section 7.2) server.route({ method: "HEAD", @@ -126,7 +127,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//new-account + // POST /api/v1/cert-manager/acme/profiles//new-account // New Account (RFC 8555 Section 7.3) server.route({ method: "POST", @@ -163,7 +164,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//accounts/ + // POST /api/v1/cert-manager/acme/profiles//accounts/ // Account Deactivation (RFC 8555 Section 7.3.6) server.route({ method: "POST", @@ -200,7 +201,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//new-order + // POST /api/v1/cert-manager/acme/profiles//new-order // New Certificate Order (RFC 8555 Section 7.4) server.route({ method: "POST", @@ -235,7 +236,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//orders/ + // POST /api/v1/cert-manager/acme/profiles//orders/ // Get Order (RFC 8555 Section 7.1.3) server.route({ method: "POST", @@ -271,7 +272,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//orders//finalize + // POST /api/v1/cert-manager/acme/profiles//orders//finalize // Applying for Certificate Issuance (RFC 8555 Section 7.4) server.route({ method: "POST", @@ -308,7 +309,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { ); } }); - // POST /api/v1/pki/acme/profiles//accounts//orders + // POST /api/v1/cert-manager/acme/profiles//accounts//orders // List Orders (RFC 8555 Section 7.1.2.1) server.route({ method: "POST", @@ -344,7 +345,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//orders//certificate + // POST /api/v1/cert-manager/acme/profiles//orders//certificate // Download Certificate (RFC 8555 Section 7.4.2) server.route({ method: "POST", @@ -377,7 +378,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//authorizations/ + // POST /api/v1/cert-manager/acme/profiles//authorizations/ // Identifier Authorization (RFC 8555 Section 7.5) server.route({ method: "POST", @@ -411,7 +412,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//authorizations//challenges/ + // POST /api/v1/cert-manager/acme/profiles//authorizations//challenges/ // Respond to Challenge (RFC 8555 Section 7.5.1) server.route({ method: "POST", diff --git a/backend/src/ee/routes/v1/project-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index c157b628b..81fd79c82 100644 --- a/backend/src/ee/routes/v1/project-template-router.ts +++ b/backend/src/ee/routes/v1/project-template-router.ts @@ -72,7 +72,6 @@ const ProjectTemplateEnvironmentsSchema = z position: z.number().min(1) }) .array() - .min(1) .superRefine((environments, ctx) => { if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" }); @@ -198,7 +197,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description), roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles), type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type), - environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional() + environments: ProjectTemplateEnvironmentsSchema.nullish().describe(ProjectTemplates.CREATE.environments) }), response: { 200: z.object({ @@ -243,7 +242,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) .describe(ProjectTemplates.UPDATE.name), description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description), roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles), - environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments) + environments: ProjectTemplateEnvironmentsSchema.nullish().describe(ProjectTemplates.UPDATE.environments) }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index f8ac34b4b..47b3f5258 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -158,6 +158,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, data: { ...req.body, + name: req.body.slug, ...req.body.type, permissions: req.body.permissions || undefined } diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index 93c3dd147..d62a1eeb2 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -1,10 +1,18 @@ +import { ProjectMembershipRole } from "@app/db/schemas"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; +import { getConfig } from "@app/lib/config/env"; +import { applyJitter } from "@app/lib/delay"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; @@ -15,7 +23,12 @@ import { TDynamicSecretLeaseConfig } from "./dynamic-secret-lease-types"; type TDynamicSecretLeaseQueueServiceFactoryDep = { queueService: TQueueServiceFactory; dynamicSecretLeaseDAL: Pick; - dynamicSecretDAL: Pick; + smtpService: Pick; + userDAL: Pick; + identityDAL: TIdentityDALFactory; + dynamicSecretDAL: Pick; + projectMembershipDAL: Pick; + projectDAL: Pick; dynamicSecretProviders: Record; kmsService: Pick; folderDAL: Pick; @@ -23,18 +36,24 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = { export type TDynamicSecretLeaseQueueServiceFactory = { pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise; - setLeaseRevocation: (leaseId: string, expiryAt: Date) => Promise; + setLeaseRevocation: (leaseId: string, dynamicSecretId: string, expiryAt: Date) => Promise; unsetLeaseRevocation: (leaseId: string) => Promise; + queueFailedRevocation: (leaseId: string, dynamicSecretId: string) => Promise; init: () => Promise; }; +const MAX_REVOCATION_RETRY_COUNT = 10; + export const dynamicSecretLeaseQueueServiceFactory = ({ queueService, dynamicSecretDAL, dynamicSecretProviders, dynamicSecretLeaseDAL, kmsService, - folderDAL + folderDAL, + projectMembershipDAL, + projectDAL, + smtpService }: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { await queueService.queuePg( @@ -48,10 +67,10 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ ); }; - const setLeaseRevocation = async (leaseId: string, expiryAt: Date) => { + const setLeaseRevocation = async (leaseId: string, dynamicSecretId: string, expiryAt: Date) => { await queueService.queuePg( QueueJobs.DynamicSecretRevocation, - { leaseId }, + { leaseId, dynamicSecretId }, { id: leaseId, singletonKey: leaseId, @@ -68,10 +87,53 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId); }; + const queueFailedRevocation = async (leaseId: string, dynamicSecretId: string) => { + const appConfig = getConfig(); + + const retryDelaySeconds = appConfig.isDevelopmentMode ? 1 : Math.floor(applyJitter(3_600_000 * 4) / 1000); // retry every 4 hours with 20% +- jitter (convert ms to seconds for pgboss) + + await queueService.queuePg( + QueueJobs.DynamicSecretRevocation, + { leaseId, isRetry: true, dynamicSecretId }, + { + singletonKey: `${leaseId}-retry`, // avoid conflicts with scheduled revocation + retryDelay: retryDelaySeconds, + retryLimit: MAX_REVOCATION_RETRY_COUNT, // we dont want it to ever hit the limit, we want the expireInHours to take effect. + expireInHours: 23 // if we set it to 24 hours, pgboss will complain that the expireIn is too high + } + ); + }; + + const $queueDynamicSecretLeaseRevocationFailedEmail = async (leaseId: string, dynamicSecretId: string) => { + const appConfig = getConfig(); + + const delay = appConfig.isDevelopmentMode ? 1_000 * 60 : 1_000 * 60 * 15; // 1 minute in development, 15 minutes in production + + await queueService.queue( + QueueName.DynamicSecretLeaseRevocationFailedEmail, + QueueJobs.DynamicSecretLeaseRevocationFailedEmail, + { + leaseId + }, + { + jobId: `dynamic-secret-lease-revocation-failed-email-${dynamicSecretId}`, + delay, + attempts: 3, + backoff: { + type: "exponential", + delay: 1000 * 60 // 1 minute + }, + removeOnComplete: true, + removeOnFail: true + } + ); + }; + const $dynamicSecretQueueJob = async ( jobName: string, jobId: string, - data: { leaseId: string } | { dynamicSecretCfgId: string } + data: { leaseId: string; dynamicSecretId: string; isRetry?: boolean } | { dynamicSecretCfgId: string }, + retryCount?: number ): Promise => { try { if (jobName === QueueJobs.DynamicSecretRevocation) { @@ -79,7 +141,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ logger.info("Dynamic secret lease revocation started: ", leaseId, jobId); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); - if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); + if (!dynamicSecretLease) { + throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); + } const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId); if (!folder) @@ -150,7 +214,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ } logger.info("Finished dynamic secret job", jobId); } catch (error) { - logger.error(error); + logger.error(error, "Failed to delete dynamic secret"); if (jobName === QueueJobs.DynamicSecretPruning) { const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string }; @@ -161,20 +225,97 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ } if (jobName === QueueJobs.DynamicSecretRevocation) { - const { leaseId } = data as { leaseId: string }; + const { leaseId, isRetry, dynamicSecretId } = data as { + leaseId: string; + isRetry?: boolean; + dynamicSecretId: string; + }; await dynamicSecretLeaseDAL.updateById(leaseId, { status: DynamicSecretStatus.FailedDeletion, - statusDetails: (error as Error)?.message?.slice(0, 255) + statusDetails: `${(error as Error)?.message?.slice(0, 255)} - Retrying automatically` }); + + // only add to retry queue if this is not a retry, and if the error is not a DisableRotationErrors error + if (!isRetry && !(error instanceof DisableRotationErrors)) { + // if revocation fails, we should stop the job and queue a new job to retry the revocation at a later time. + await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId); + await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId); + await queueFailedRevocation(leaseId, dynamicSecretId); + + // if its the last attempt, and the error isn't a DisableRotationErrors error, send an email to the project admins (debounced) + } else if (isRetry && !(error instanceof DisableRotationErrors)) { + if (retryCount && retryCount === MAX_REVOCATION_RETRY_COUNT) { + // if all retries fail, we should also stop the automatic revocation job. + // the ID of the revocation job is set to the leaseId, so we can use that to stop the job + + // we dont have to stop the retry job, because if we hit this point, its the last attempt and the retry job will be stopped by pgboss itself after this point, + await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId); + await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, leaseId); + + await $queueDynamicSecretLeaseRevocationFailedEmail(leaseId, dynamicSecretId); + } + } } if (error instanceof DisableRotationErrors) { if (jobId) { await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId); await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId); } + } else { + // propagate to next part + throw error; + } + } + }; + + // send alert email once all revocation attempts have failed + const $dynamicSecretLeaseRevocationFailedEmailJob = async (jobId: string, data: { leaseId: string }) => { + try { + const appCfg = getConfig(); + + const { leaseId } = data; + logger.info( + { leaseId, jobId }, + "Dynamic secret revocation failed. Notifying project admins about failed revocation." + ); + + const lease = await dynamicSecretLeaseDAL.findById(leaseId); + if (!lease) { + throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); + } + + const folder = await folderDAL.findById(lease.dynamicSecret.folderId); + if (!folder) throw new NotFoundError({ message: `Failed to find folder with ${lease.dynamicSecret.folderId}` }); + + const project = await projectDAL.findById(folder.projectId); + const projectMembers = await projectMembershipDAL.findAllProjectMembers(project.id); + + const projectAdmins = projectMembers.filter((member) => + member.roles.some((role) => role.role === ProjectMembershipRole.Admin) + ); + + await smtpService.sendMail({ + recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), + template: SmtpTemplates.DynamicSecretLeaseRevocationFailed, + subjectLine: "Dynamic Secret Lease Revocation Failed", + substitutions: { + dynamicSecretLeaseUrl: `${appCfg.SITE_URL}/organizations/${project.orgId}/projects/secret-management/${project.id}/secrets/${folder.environment.envSlug}?dynamicSecretId=${lease.dynamicSecret.id}&filterBy=dynamic&search=${lease.dynamicSecret.name}`, + dynamicSecretName: lease.dynamicSecret.name, + projectName: project.name, + environmentSlug: folder.environment.envSlug, + errorMessage: lease.statusDetails || "An unknown error occurred" + } + }); + } catch (error) { + logger.error(error, "Failed to send dynamic secret lease revocation failed email"); + if (error instanceof DisableRotationErrors) { + if (jobId) { + await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretLeaseRevocationFailedEmail, jobId); + await queueService.stopJobById(QueueName.DynamicSecretLeaseRevocationFailedEmail, jobId); + } + } else { + throw error; } - // propogate to next part - throw error; } }; @@ -182,14 +323,21 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ await $dynamicSecretQueueJob(job.name, job.id as string, job.data); }); + // we use redis for sending the email because: + // 1. we are insensitive to losing the jobs in queue in case of a disaster event + // 2. pgboss does not support exclusive job keys on v0.10.x, and upgrading to v0.11.x which supports exclusive jobs comes with a lot of breaking changes, and we would need to manually migrate our existing jobs to the new version + queueService.start(QueueName.DynamicSecretLeaseRevocationFailedEmail, async (job) => { + await $dynamicSecretLeaseRevocationFailedEmailJob(job.id as string, job.data); + }); + const init = async () => { await queueService.startPg( QueueJobs.DynamicSecretRevocation, async ([job]) => { - await $dynamicSecretQueueJob(job.name, job.id, job.data); + await $dynamicSecretQueueJob(job.name, job.id, job.data, job.retryCount); }, { - workerCount: 5, + workerCount: 10, pollingIntervalSeconds: 1 } ); @@ -210,6 +358,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ pruneDynamicSecret, setLeaseRevocation, unsetLeaseRevocation, + queueFailedRevocation, init }; }; diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index cf37626c7..ea5efd502 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -178,7 +178,7 @@ export const dynamicSecretLeaseServiceFactory = ({ config }); - await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt); + await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, dynamicSecretCfg.id, expireAt); return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data }; }; @@ -272,7 +272,7 @@ export const dynamicSecretLeaseServiceFactory = ({ ); await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); - await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt); + await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, dynamicSecretCfg.id, expireAt); const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { expireAt, externalEntityId: entityId @@ -358,11 +358,13 @@ export const dynamicSecretLeaseServiceFactory = ({ if ((revokeResponse as { error?: Error })?.error) { const { error } = revokeResponse as { error?: Error }; logger.error(error?.message, "Failed to revoke lease"); - const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { + const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { status: DynamicSecretLeaseStatus.FailedDeletion, statusDetails: error?.message?.slice(0, 255) }); - return deletedDynamicSecretLease; + // queue a job to retry the revocation at a later time + await dynamicSecretQueueService.queueFailedRevocation(dynamicSecretLease.id, dynamicSecretCfg.id); + return updatedDynamicSecretLease; } await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); diff --git a/backend/src/ee/services/pki-acme/pki-acme-fns.ts b/backend/src/ee/services/pki-acme/pki-acme-fns.ts index a5206d036..759e3cdf9 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-fns.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-fns.ts @@ -8,7 +8,7 @@ import { AcmeAccountDoesNotExistError } from "./pki-acme-errors"; export const buildUrl = (profileId: string, path: string): string => { const appCfg = getConfig(); const baseUrl = appCfg.SITE_URL ?? ""; - return `${baseUrl}/api/v1/pki/acme/profiles/${profileId}${path}`; + return `${baseUrl}/api/v1/cert-manager/acme/profiles/${profileId}${path}`; }; export const extractAccountIdFromKid = (kid: string, profileId: string): string => { diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 71b7a7b70..ace2d995b 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -776,8 +776,9 @@ export const pkiAcmeServiceFactory = ({ const cert = await orderCertificate( { caId: certificateAuthority!.id, - profileId, - 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 diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index 5a9f04d8d..1ba21873a 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -189,11 +189,15 @@ export const projectTemplateServiceFactory = ({ message: `A project template with the name "${params.name}" already exists.` }); + const projectTemplateEnvironments = + type === ProjectType.SecretManager && environments === undefined + ? ProjectTemplateDefaultEnvironments + : environments; + const projectTemplate = await projectTemplateDAL.create({ ...params, roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))), - environments: - type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null, + environments: JSON.stringify(projectTemplateEnvironments), orgId: actor.orgId, type }); diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 7206bd293..38411627a 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -622,7 +622,7 @@ export const samlConfigServiceFactory = ({ const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL); newUser = await userDAL.create( { - username: serverCfg.trustSamlEmails ? email : uniqueUsername, + username: serverCfg.trustSamlEmails ? email.toLowerCase() : uniqueUsername, email, isEmailVerified: serverCfg.trustSamlEmails, firstName, @@ -639,7 +639,7 @@ export const samlConfigServiceFactory = ({ userId: newUser.id, aliasType: UserAliasType.SAML, externalId, - emails: email ? [email] : [], + emails: email ? [email.toLowerCase()] : [], orgId, isEmailVerified: serverCfg.trustSamlEmails }, diff --git a/backend/src/ee/services/scim/scim-dal.ts b/backend/src/ee/services/scim/scim-dal.ts index 77a19d4d2..e856070e1 100644 --- a/backend/src/ee/services/scim/scim-dal.ts +++ b/backend/src/ee/services/scim/scim-dal.ts @@ -1,10 +1,56 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify, TOrmify } from "@app/lib/knex"; +import { AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; -export type TScimDALFactory = TOrmify; +import { TExpiringScimToken } from "./scim-types"; -export const scimDALFactory = (db: TDbClient): TScimDALFactory => { +export type TScimDALFactory = ReturnType; + +export const scimDALFactory = (db: TDbClient) => { const scimTokenOrm = ormify(db, TableName.ScimToken); - return scimTokenOrm; + + const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0): Promise => { + try { + const batch = await (tx || db.replicaNode())(TableName.ScimToken) + .leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.ScimToken}.orgId`) + .leftJoin(TableName.Membership, `${TableName.Membership}.scopeOrgId`, `${TableName.ScimToken}.orgId`) + .leftJoin(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .whereRaw( + ` + (${TableName.ScimToken}."ttlDays" > 0 AND + (${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") < NOW() + INTERVAL '7 days' AND + (${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") > NOW()) + ` + ) + .where(`${TableName.ScimToken}.expiryNotificationSent`, false) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.MembershipRole}.role`, OrgMembershipRole.Admin) + .whereNot(`${TableName.Membership}.status`, OrgMembershipStatus.Invited) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .where(`${TableName.Users}.isGhost`, false) + .whereNotNull(`${TableName.Users}.email`) + .groupBy([`${TableName.ScimToken}.id`, `${TableName.Organization}.name`]) + .select([ + db.ref("id").withSchema(TableName.ScimToken), + db.ref("ttlDays").withSchema(TableName.ScimToken), + db.ref("description").withSchema(TableName.ScimToken), + db.ref("orgId").withSchema(TableName.ScimToken), + db.ref("createdAt").withSchema(TableName.ScimToken), + db.ref("name").withSchema(TableName.Organization).as("orgName"), + db.raw(`array_agg(${TableName.Users}."email") as "adminEmails"`) + ]) + .limit(batchSize) + .offset(offset); + + return batch; + } catch (err) { + throw new DatabaseError({ error: err, name: "FindExpiringTokens" }); + } + }; + + return { ...scimTokenOrm, findExpiringTokens }; }; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 8b9256023..465ed3ee5 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -19,6 +19,7 @@ import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal"; import { AuthTokenType } from "@app/services/auth/auth-type"; @@ -47,7 +48,7 @@ import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, p import { TScimGroup, TScimServiceFactory } from "./scim-types"; type TScimServiceFactoryDep = { - scimDAL: Pick; + scimDAL: Pick; userDAL: Pick< TUserDALFactory, "find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById" @@ -389,15 +390,13 @@ export const scimServiceFactory = ({ ); } } else { - if (trustScimEmails) { - user = await userDAL.findOne( - { - email: email.toLowerCase(), - isEmailVerified: true - }, - tx - ); - } + user = await userDAL.findOne( + { + email: email.toLowerCase(), + isEmailVerified: true + }, + tx + ); if (!user) { const uniqueUsername = await normalizeUsername( @@ -425,7 +424,8 @@ export const scimServiceFactory = ({ aliasType, externalId, emails: email ? [email.toLowerCase()] : [], - orgId + orgId, + isEmailVerified: trustScimEmails }, tx ); @@ -1237,6 +1237,70 @@ export const scimServiceFactory = ({ return { scimTokenId: scimToken.id, orgId: scimToken.orgId }; }; + const notifyExpiringTokens: TScimServiceFactory["notifyExpiringTokens"] = async () => { + const appCfg = getConfig(); + let processedCount = 0; + let hasMoreRecords = true; + let offset = 0; + const batchSize = 500; + + while (hasMoreRecords) { + // eslint-disable-next-line no-await-in-loop + const expiringTokens = await scimDAL.findExpiringTokens(undefined, batchSize, offset); + + if (expiringTokens.length === 0) { + hasMoreRecords = false; + break; + } + + const successfullyNotifiedTokenIds: string[] = []; + + // eslint-disable-next-line no-await-in-loop + await Promise.all( + expiringTokens.map(async (token) => { + try { + if (token.adminEmails.length === 0) { + // Still mark as notified to avoid repeated checks + successfullyNotifiedTokenIds.push(token.id); + return; + } + + const createdOn = new Date(token.createdAt); + const expiringOn = new Date(createdOn.getTime() + Number(token.ttlDays) * 86400 * 1000); + + await smtpService.sendMail({ + recipients: token.adminEmails, + subjectLine: "SCIM Token Expiry Notice", + template: SmtpTemplates.ScimTokenExpired, + substitutions: { + tokenDescription: token.description, + orgName: token.orgName, + url: `${appCfg.SITE_URL}/organizations/${token.orgId}/settings?selectedTab=provisioning-settings`, + createdOn, + expiringOn + } + }); + + successfullyNotifiedTokenIds.push(token.id); + } catch (error) { + logger.error(error, `Failed to send expiration notification for SCIM token ${token.id}:`); + } + }) + ); + + // Batch update all successfully notified tokens in a single query + if (successfullyNotifiedTokenIds.length > 0) { + // eslint-disable-next-line no-await-in-loop + await scimDAL.update({ $in: { id: successfullyNotifiedTokenIds } }, { expiryNotificationSent: true }); + } + + processedCount += expiringTokens.length; + offset += batchSize; + } + + return processedCount; + }; + return { createScimToken, listScimTokens, @@ -1253,6 +1317,7 @@ export const scimServiceFactory = ({ deleteScimGroup, replaceScimGroup, updateScimGroup, - fnValidateScimToken + fnValidateScimToken, + notifyExpiringTokens }; }; diff --git a/backend/src/ee/services/scim/scim-types.ts b/backend/src/ee/services/scim/scim-types.ts index 8bdea39e1..1275ef283 100644 --- a/backend/src/ee/services/scim/scim-types.ts +++ b/backend/src/ee/services/scim/scim-types.ts @@ -158,6 +158,16 @@ export type TScimGroup = { }; }; +export type TExpiringScimToken = { + id: string; + ttlDays: number; + description: string; + orgId: string; + createdAt: Date; + orgName: string; + adminEmails: string[]; +}; + export type TScimServiceFactory = { createScimToken: (arg: TCreateScimTokenDTO) => Promise<{ scimToken: string; @@ -200,4 +210,5 @@ export type TScimServiceFactory = { scimTokenId: string; orgId: string; }>; + notifyExpiringTokens: () => Promise; }; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 970f9a1a0..3c8972248 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1962,9 +1962,11 @@ export const CERTIFICATE_AUTHORITIES = { export const CERTIFICATES = { GET: { + id: "The ID of the certificate to get.", serialNumber: "The serial number of the certificate to get." }, REVOKE: { + id: "The ID of the certificate to revoke.", serialNumber: "The serial number of the certificate to revoke. The revoked certificate will be added to the certificate revocation list (CRL) of the CA.", revocationReason: "The reason for revoking the certificate.", @@ -1972,9 +1974,11 @@ export const CERTIFICATES = { serialNumberRes: "The serial number of the revoked certificate." }, DELETE: { + id: "The ID of the certificate to delete.", serialNumber: "The serial number of the certificate to delete." }, GET_CERT: { + id: "The ID of the certificate to get the certificate body and certificate chain for.", serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.", certificate: "The certificate body of the certificate.", certificateChain: "The certificate chain of the certificate.", diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 11de57667..21e83c2b7 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -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"), diff --git a/backend/src/lib/delay/index.ts b/backend/src/lib/delay/index.ts index 32cb8ebfc..a5d4250fc 100644 --- a/backend/src/lib/delay/index.ts +++ b/backend/src/lib/delay/index.ts @@ -2,3 +2,13 @@ export const delay = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms); }); + +export const applyJitter = (delayMs: number) => { + const jitterFactor = 0.2; + + // generates random value in [-0.2, +0.2] range + const randomFactor = (Math.random() * 2 - 1) * jitterFactor; + const jitterAmount = randomFactor * delayMs; + + return delayMs + jitterAmount; +}; diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index a7ed3a170..f922918f9 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -61,6 +61,7 @@ export enum QueueName { SecretPushEventScan = "secret-push-event-scan", UpgradeProjectToGhost = "upgrade-project-to-ghost", DynamicSecretRevocation = "dynamic-secret-revocation", + DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email", CaCrlRotation = "ca-crl-rotation", CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber CertificateIssuance = "certificate-issuance", @@ -121,6 +122,7 @@ export enum QueueJobs { SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", SecretRotationV2SendNotification = "secret-rotation-v2-send-notification", CreateFolderTreeCheckpoint = "create-folder-tree-checkpoint", + DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email", InvalidateCache = "invalidate-cache", SecretScanningV2FullScan = "secret-scanning-v2-full-scan", SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan", @@ -221,11 +223,19 @@ export type TQueueJobTypes = { name: QueueJobs.TelemetryInstanceStats; payload: undefined; }; + [QueueName.DynamicSecretLeaseRevocationFailedEmail]: { + name: QueueJobs.DynamicSecretLeaseRevocationFailedEmail; + payload: { + leaseId: string; + }; + }; [QueueName.DynamicSecretRevocation]: | { name: QueueJobs.DynamicSecretRevocation; payload: { + isRetry?: boolean; leaseId: string; + dynamicSecretId: string; }; } | { diff --git a/backend/src/server/plugins/add-errors-to-response-schemas.ts b/backend/src/server/plugins/add-errors-to-response-schemas.ts index 6337bae0f..a09f34a0e 100644 --- a/backend/src/server/plugins/add-errors-to-response-schemas.ts +++ b/backend/src/server/plugins/add-errors-to-response-schemas.ts @@ -6,7 +6,7 @@ import { DefaultResponseErrorsSchema } from "../routes/sanitizedSchemas"; const isScimRoutes = (pathname: string) => pathname.startsWith("/api/v1/scim/Users") || pathname.startsWith("/api/v1/scim/Groups"); -const isAcmeRoutes = (pathname: string) => pathname.startsWith("/api/v1/pki/acme/"); +const isAcmeRoutes = (pathname: string) => pathname.startsWith("/api/v1/cert-manager/acme/"); export const addErrorsToResponseSchemas = fp(async (server) => { server.addHook("onRoute", (routeOptions) => { diff --git a/backend/src/server/plugins/serve-ui.ts b/backend/src/server/plugins/serve-ui.ts index b71451b6e..633b4211a 100644 --- a/backend/src/server/plugins/serve-ui.ts +++ b/backend/src/server/plugins/serve-ui.ts @@ -43,7 +43,9 @@ export const registerServeUI = async ( const frontendPath = path.join(dir, frontendName); await server.register(staticServe, { root: frontendPath, - wildcard: false + wildcard: false, + maxAge: "30d", + immutable: true }); server.route({ @@ -58,11 +60,12 @@ export const registerServeUI = async ( return; } - // This should help avoid caching any chunks (temp fix) - void reply.header("Cache-Control", "no-cache, no-store, must-revalidate, private, max-age=0"); - void reply.header("Pragma", "no-cache"); - void reply.header("Expires", "0"); - return reply.sendFile("index.html"); + return reply.sendFile("index.html", { + immutable: false, + maxAge: 0, + lastModified: false, + etag: false + }); } }); } diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4d5a498a0..2ff71bc3a 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1333,7 +1333,8 @@ export const registerRoutes = async ( eventBusService, licenseService, membershipRoleDAL, - membershipUserDAL + membershipUserDAL, + telemetryService }); const projectService = projectServiceFactory({ @@ -1878,7 +1879,12 @@ export const registerRoutes = async ( dynamicSecretProviders, dynamicSecretDAL, folderDAL, - kmsService + kmsService, + smtpService, + userDAL, + identityDAL, + projectMembershipDAL, + projectDAL }); const dynamicSecretService = dynamicSecretServiceFactory({ projectDAL, @@ -1911,6 +1917,7 @@ export const registerRoutes = async ( // DAILY const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ + scimService, auditLogDAL, queueService, secretVersionDAL, diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 5a3496750..48fdc7c38 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -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) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/dns-made-easy-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/dns-made-easy-connection-router.ts new file mode 100644 index 000000000..e1e0b2860 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/dns-made-easy-connection-router.ts @@ -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; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index aa1d671b6..d7a4065fd 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -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 { actorAuthMethod: req.permission.authMethod, isInternal: false, actorOrgId: req.permission.orgId, - enableDirectIssuance: !req.body.requireTemplateForIssuance, ...req.body }); @@ -220,7 +219,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { isInternal: false, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - enableDirectIssuance: !req.body.requireTemplateForIssuance, ...req.body }); @@ -617,6 +615,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + // TODO: DEPRECATE server.route({ method: "POST", url: "/:caId/issue-certificate", @@ -625,7 +624,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - hide: false, tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Issue certificate from CA", params: z.object({ @@ -711,6 +709,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + // TODO: DEPRECATE server.route({ method: "POST", url: "/:caId/sign-certificate", @@ -719,7 +718,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - hide: false, tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Sign certificate from CA", params: z.object({ @@ -805,6 +803,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + // TODO: DEPRECATE server.route({ method: "GET", url: "/:caId/certificate-templates", @@ -813,7 +812,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - hide: false, tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get list of certificate templates for the CA", params: z.object({ @@ -854,6 +852,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + // TODO: DEPRECATE server.route({ method: "GET", url: "/:caId/crls", @@ -862,7 +861,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - hide: false, tags: [ApiDocsTags.PkiCertificateAuthorities], description: "Get list of CRLs of the CA", params: z.object({ diff --git a/backend/src/server/routes/v1/certificate-authority-routers/certificate-authority-endpoints.ts b/backend/src/server/routes/v1/certificate-authority-routers/certificate-authority-endpoints.ts index 01952c7f4..7e89a99a1 100644 --- a/backend/src/server/routes/v1/certificate-authority-routers/certificate-authority-endpoints.ts +++ b/backend/src/server/routes/v1/certificate-authority-routers/certificate-authority-endpoints.ts @@ -28,14 +28,10 @@ export const registerCertificateAuthorityEndpoints = < projectId: string; status: CaStatus; configuration: I["configuration"]; - enableDirectIssuance: boolean; }>; updateSchema: z.ZodType<{ - projectId: string; - name?: string; status?: CaStatus; configuration?: I["configuration"]; - enableDirectIssuance?: boolean; }>; responseSchema: z.ZodTypeAny; }) => { @@ -83,7 +79,7 @@ export const registerCertificateAuthorityEndpoints = < server.route({ method: "GET", - url: "/:caName", + url: "/:id", config: { rateLimit: readLimit }, @@ -91,10 +87,7 @@ export const registerCertificateAuthorityEndpoints = < hide: false, tags: [ApiDocsTags.PkiCertificateAuthorities], params: z.object({ - caName: z.string() - }), - querystring: z.object({ - projectId: z.string().uuid() + id: z.string() }), response: { 200: responseSchema @@ -102,14 +95,12 @@ export const registerCertificateAuthorityEndpoints = < }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const { caName } = req.params; - const { projectId } = req.query; + const { id } = req.params; - const certificateAuthority = - (await server.services.certificateAuthority.findCertificateAuthorityByNameAndProjectId( - { caName, type: caType, projectId }, - req.permission - )) as T; + const certificateAuthority = (await server.services.certificateAuthority.findCertificateAuthorityById( + { id, type: caType }, + req.permission + )) as T; await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -166,7 +157,7 @@ export const registerCertificateAuthorityEndpoints = < server.route({ method: "PATCH", - url: "/:caName", + url: "/:id", config: { rateLimit: writeLimit }, @@ -174,7 +165,7 @@ export const registerCertificateAuthorityEndpoints = < hide: false, tags: [ApiDocsTags.PkiCertificateAuthorities], params: z.object({ - caName: z.string() + id: z.string() }), body: updateSchema, response: { @@ -183,13 +174,13 @@ export const registerCertificateAuthorityEndpoints = < }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const { caName } = req.params; + const { id } = req.params; const certificateAuthority = (await server.services.certificateAuthority.updateCertificateAuthority( { ...req.body, type: caType, - caName + id }, req.permission )) as T; @@ -213,7 +204,7 @@ export const registerCertificateAuthorityEndpoints = < server.route({ method: "DELETE", - url: "/:caName", + url: "/:id", config: { rateLimit: writeLimit }, @@ -221,10 +212,7 @@ export const registerCertificateAuthorityEndpoints = < hide: false, tags: [ApiDocsTags.PkiCertificateAuthorities], params: z.object({ - caName: z.string() - }), - body: z.object({ - projectId: z.string().uuid() + id: z.string() }), response: { 200: responseSchema @@ -232,11 +220,10 @@ export const registerCertificateAuthorityEndpoints = < }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const { caName } = req.params; - const { projectId } = req.body; + const { id } = req.params; const certificateAuthority = (await server.services.certificateAuthority.deleteCertificateAuthority( - { caName, type: caType, projectId }, + { id, type: caType }, req.permission )) as T; diff --git a/backend/src/server/routes/v1/certificate-authority-routers/general-certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-routers/general-certificate-authority-router.ts new file mode 100644 index 000000000..7a7281d57 --- /dev/null +++ b/backend/src/server/routes/v1/certificate-authority-routers/general-certificate-authority-router.ts @@ -0,0 +1,85 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags } from "@app/lib/api-docs"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas"; +import { AzureAdCsCertificateAuthoritySchema } from "@app/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas"; +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { InternalCertificateAuthoritySchema } from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas"; + +const CertificateAuthoritySchema = z.discriminatedUnion("type", [ + InternalCertificateAuthoritySchema, + AcmeCertificateAuthoritySchema, + AzureAdCsCertificateAuthoritySchema +]); + +export const registerGeneralCertificateAuthorityRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + description: "Get Certificate Authorities", + querystring: z.object({ + projectId: z.string() + }), + response: { + 200: z.object({ + certificateAuthorities: CertificateAuthoritySchema.array() + }) + } + }, + handler: async (req) => { + const internalCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId( + { + projectId: req.query.projectId, + type: CaType.INTERNAL + }, + req.permission + ); + + const acmeCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId( + { + projectId: req.query.projectId, + type: CaType.ACME + }, + req.permission + ); + + const azureAdCsCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId( + { + projectId: req.query.projectId, + type: CaType.AZURE_AD_CS + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.GET_CAS, + metadata: { + caIds: [ + ...(internalCas ?? []).map((ca) => ca.id), + ...(acmeCas ?? []).map((ca) => ca.id), + ...(azureAdCsCas ?? []).map((ca) => ca.id) + ] + } + } + }); + + return { + certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? []), ...(azureAdCsCas ?? [])] + }; + } + }); +}; diff --git a/backend/src/server/routes/v1/certificate-authority-routers/internal-certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-routers/internal-certificate-authority-router.ts index 61dc3ed57..73e3bde54 100644 --- a/backend/src/server/routes/v1/certificate-authority-routers/internal-certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-routers/internal-certificate-authority-router.ts @@ -1,4 +1,12 @@ -import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { CaRenewalType, CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; import { CreateInternalCertificateAuthoritySchema, InternalCertificateAuthoritySchema, @@ -15,4 +23,406 @@ export const registerInternalCertificateAuthorityRouter = async (server: Fastify createSchema: CreateInternalCertificateAuthoritySchema, updateSchema: UpdateInternalCertificateAuthoritySchema }); + + server.route({ + method: "GET", + url: "/:caId/csr", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + description: "Get CA CSR", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CSR.caId) + }), + response: { + 200: z.object({ + csr: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CSR.csr) + }) + } + }, + handler: async (req) => { + const { ca, csr } = await server.services.internalCertificateAuthority.getCaCsr({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CA_CSR, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); + + return { + csr + }; + } + }); + + server.route({ + method: "POST", + url: "/:caId/renew", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + description: "Perform CA certificate renewal", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId) + }), + body: z.object({ + type: z.nativeEnum(CaRenewalType).describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.type), + notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.notAfter) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificate), + certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificateChain), + serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, serialNumber, ca } = + await server.services.internalCertificateAuthority.renewCaCert({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.RENEW_CA, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); + + return { + certificate, + certificateChain, + serialNumber + }; + } + }); + + server.route({ + method: "GET", + url: "/:caId/ca-certificates", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + description: "Get list of past and current CA certificates for a CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.caId) + }), + response: { + 200: z.array( + z.object({ + certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificate), + certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificateChain), + serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.serialNumber), + version: z.number().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.version) + }) + ) + } + }, + handler: async (req) => { + const { caCerts, ca } = await server.services.internalCertificateAuthority.getCaCerts({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CA_CERTS, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); + + return caCerts; + } + }); + + server.route({ + method: "GET", + url: "/:caId/certificate", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + description: "Get current CA cert and cert chain of a CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId) + }), + response: { + 200: z.object({ + certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificate), + certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificateChain), + serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, serialNumber, ca } = + await server.services.internalCertificateAuthority.getCaCert({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CA_CERT, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); + + return { + certificate, + certificateChain, + serialNumber + }; + } + }); + + server.route({ + method: "POST", + url: "/:caId/sign-intermediate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + description: "Create intermediate CA certificate from parent CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId) + }), + body: z.object({ + csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr), + notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore), + notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter), + maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.certificate), + certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.certificateChain), + issuingCaCertificate: z + .string() + .trim() + .describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.issuingCaCertificate), + serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } = + await server.services.internalCertificateAuthority.signIntermediate({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.SIGN_INTERMEDIATE, + metadata: { + caId: ca.id, + dn: ca.dn, + serialNumber + } + } + }); + + return { + certificate, + certificateChain, + issuingCaCertificate, + serialNumber + }; + } + }); + + server.route({ + method: "POST", + url: "/:caId/import-certificate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + description: "Import certificate and chain to CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.caId) + }), + body: z.object({ + certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.certificate), + certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.certificateChain) + }), + response: { + 200: z.object({ + message: z.string().trim(), + caId: z.string().trim() + }) + } + }, + handler: async (req) => { + const { ca } = await server.services.internalCertificateAuthority.importCertToCa({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.IMPORT_CA_CERT, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); + + return { + message: "Successfully imported certificate to CA", + caId: req.params.caId + }; + } + }); + + server.route({ + method: "GET", + url: "/:caId/crls", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + description: "Get list of CRLs of the CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.caId) + }), + response: { + 200: z.array( + z.object({ + id: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.id), + crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.crl) + }) + ) + } + }, + handler: async (req) => { + const { ca, crls } = await server.services.certificateAuthorityCrl.getCaCrls({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CA_CRLS, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); + + return crls; + } + }); + + // this endpoint will be used to serve the CA certificate when a client makes a request + // against the Authority Information Access CA Issuer URL + server.route({ + method: "GET", + url: "/:caId/certificates/:caCertId/der", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + description: "Get DER-encoded certificate of CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caId), + caCertId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caCertId) + }), + response: { + 200: z.instanceof(Buffer) + } + }, + handler: async (req, res) => { + const caCert = await server.services.internalCertificateAuthority.getCaCertById(req.params); + + void res.header("Content-Type", "application/pkix-cert"); + + return Buffer.from(caCert.rawData); + } + }); }; diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index e8cdbb540..a4af7fd92 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -4,24 +4,510 @@ import { z } from "zod"; import { CertificatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs"; +import { ApiDocsTags, CERTIFICATES } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { addNoCacheHeaders } from "@app/server/lib/caching"; -import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types"; import { - validateAltNamesField, - validateCaDateField -} from "@app/services/certificate-authority/certificate-authority-validators"; -import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + ACMESANType, + CertificateOrderStatus, + CertKeyAlgorithm, + CertSignatureAlgorithm, + CrlReason +} from "@app/services/certificate/certificate-types"; +import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; +import { + CertExtendedKeyUsageType, + CertKeyUsageType, + CertSubjectAlternativeNameType +} from "@app/services/certificate-common/certificate-constants"; +import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils"; +import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils"; +import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; +import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; + +import { booleanSchema } from "../sanitizedSchemas"; + +interface CertificateRequestForService { + commonName?: string; + keyUsages?: CertKeyUsageType[]; + extendedKeyUsages?: CertExtendedKeyUsageType[]; + altNames?: Array<{ + type: CertSubjectAlternativeNameType; + value: string; + }>; + validity: { + ttl: string; + }; + notBefore?: Date; + notAfter?: Date; + signatureAlgorithm?: string; + keyAlgorithm?: string; +} + +const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => { + const hasDateFields = data.notBefore || data.notAfter; + const hasTtl = data.ttl; + return !(hasDateFields && hasTtl); +}; + +const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => { + if (data.notBefore && data.notAfter) { + const notBefore = new Date(data.notBefore); + const notAfter = new Date(data.notAfter); + return notBefore < notAfter; + } + return true; +}; + +export const registerCertificateRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/issue-certificate", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + body: z + .object({ + profileId: z.string().uuid(), + commonName: validateTemplateRegexField.optional(), + ttl: z + .string() + .trim() + .min(1, "TTL cannot be empty") + .refine((val) => ms(val) > 0, "TTL must be a positive number"), + keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(), + notBefore: validateCaDateField.optional(), + notAfter: validateCaDateField.optional(), + altNames: z + .array( + z.object({ + type: z.nativeEnum(CertSubjectAlternativeNameType), + value: z.string().min(1, "SAN value cannot be empty") + }) + ) + .optional(), + signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm), + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), + removeRootsFromChain: booleanSchema.default(false).optional() + }) + .refine(validateTtlAndDateFields, { + message: + "Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range." + }) + .refine(validateDateOrder, { + message: "notBefore must be earlier than notAfter" + }), + response: { + 200: z.object({ + certificate: z.string().trim(), + issuingCaCertificate: z.string().trim(), + certificateChain: z.string().trim(), + privateKey: z.string().trim().optional(), + serialNumber: z.string().trim(), + certificateId: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateRequestForService: CertificateRequestForService = { + commonName: req.body.commonName, + keyUsages: req.body.keyUsages, + extendedKeyUsages: req.body.extendedKeyUsages, + altNames: req.body.altNames, + validity: { + ttl: req.body.ttl + }, + notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined, + notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined, + signatureAlgorithm: req.body.signatureAlgorithm, + keyAlgorithm: req.body.keyAlgorithm + }; + + const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService); + + const data = await server.services.certificateV3.issueCertificateFromProfile({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + profileId: req.body.profileId, + certificateRequest: mappedCertificateRequest, + removeRootsFromChain: req.body.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE, + metadata: { + certificateProfileId: req.body.profileId, + certificateId: data.certificateId, + commonName: req.body.commonName || "", + profileName: data.profileName + } + } + }); + + return data; + } + }); + + server.route({ + method: "POST", + url: "/sign-certificate", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + body: z + .object({ + profileId: z.string().uuid(), + csr: z.string().trim().min(1, "CSR cannot be empty").max(4096, "CSR cannot exceed 4096 characters"), + ttl: z + .string() + .trim() + .min(1, "TTL cannot be empty") + .refine((val) => ms(val) > 0, "TTL must be a positive number"), + notBefore: validateCaDateField.optional(), + notAfter: validateCaDateField.optional(), + removeRootsFromChain: booleanSchema.default(false).optional() + }) + .refine(validateTtlAndDateFields, { + message: + "Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range." + }) + .refine(validateDateOrder, { + message: "notBefore must be earlier than notAfter" + }), + response: { + 200: z.object({ + certificate: z.string().trim(), + issuingCaCertificate: z.string().trim(), + certificateChain: z.string().trim(), + serialNumber: z.string().trim(), + certificateId: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateRequest = extractCertificateRequestFromCSR(req.body.csr); + + const data = await server.services.certificateV3.signCertificateFromProfile({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + profileId: req.body.profileId, + csr: req.body.csr, + validity: { + ttl: req.body.ttl + }, + notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined, + notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined, + enrollmentType: EnrollmentType.API, + removeRootsFromChain: req.body.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.SIGN_CERTIFICATE_FROM_PROFILE, + metadata: { + certificateProfileId: req.body.profileId, + certificateId: data.certificateId, + profileName: data.profileName, + commonName: certificateRequest.commonName || "" + } + } + }); + + return data; + } + }); + + server.route({ + method: "POST", + url: "/order-certificate", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + body: z + .object({ + profileId: z.string().uuid(), + subjectAlternativeNames: z + .array( + z.object({ + type: z.nativeEnum(ACMESANType), + value: z + .string() + .trim() + .min(1, "SAN value cannot be empty") + .max(255, "SAN value must be less than 255 characters") + }) + ) + .min(1, "At least one subject alternative name must be provided"), + ttl: z + .string() + .trim() + .min(1, "TTL cannot be empty") + .refine((val) => ms(val) > 0, "TTL must be a positive number"), + keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(), + notBefore: validateCaDateField.optional(), + notAfter: validateCaDateField.optional(), + commonName: validateTemplateRegexField.optional(), + signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm), + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), + removeRootsFromChain: booleanSchema.default(false).optional() + }) + .refine(validateTtlAndDateFields, { + message: + "Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range." + }) + .refine(validateDateOrder, { + message: "notBefore must be earlier than notAfter" + }), + response: { + 200: z.object({ + orderId: z.string(), + status: z.nativeEnum(CertificateOrderStatus), + subjectAlternativeNames: z.array( + z.object({ + type: z.nativeEnum(ACMESANType), + value: z.string(), + status: z.nativeEnum(CertificateOrderStatus) + }) + ), + authorizations: z.array( + z.object({ + identifier: z.object({ + type: z.nativeEnum(ACMESANType), + value: z.string() + }), + status: z.nativeEnum(CertificateOrderStatus), + expires: z.string().optional(), + challenges: z.array( + z.object({ + type: z.string(), + status: z.nativeEnum(CertificateOrderStatus), + url: z.string(), + token: z.string() + }) + ) + }) + ), + finalize: z.string(), + certificate: z.string().optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const data = await server.services.certificateV3.orderCertificateFromProfile({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + profileId: req.body.profileId, + certificateOrder: { + altNames: req.body.subjectAlternativeNames, + validity: { + ttl: req.body.ttl + }, + commonName: req.body.commonName, + keyUsages: req.body.keyUsages, + extendedKeyUsages: req.body.extendedKeyUsages, + notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined, + notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined, + signatureAlgorithm: req.body.signatureAlgorithm, + keyAlgorithm: req.body.keyAlgorithm + }, + removeRootsFromChain: req.body.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, + metadata: { + certificateProfileId: req.body.profileId, + orderId: data.orderId, + profileName: data.profileName + } + } + }); + + return data; + } + }); + + server.route({ + method: "POST", + url: "/:id/renew", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + params: z.object({ + id: z.string().uuid() + }), + body: z + .object({ + removeRootsFromChain: booleanSchema.default(false).optional() + }) + .optional(), + response: { + 200: z.object({ + certificate: z.string().trim(), + issuingCaCertificate: z.string().trim(), + certificateChain: z.string().trim(), + privateKey: z.string().trim().optional(), + serialNumber: z.string().trim(), + certificateId: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const data = await server.services.certificateV3.renewCertificate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + certificateId: req.params.id, + removeRootsFromChain: req.body?.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.RENEW_CERTIFICATE, + metadata: { + originalCertificateId: req.params.id, + newCertificateId: data.certificateId, + profileName: data.profileName, + commonName: data.commonName + } + } + }); + + return data; + } + }); + + server.route({ + method: "PATCH", + url: "/:id/config", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + params: z.object({ + id: z.string().uuid() + }), + body: z + .object({ + renewBeforeDays: z.number().int().min(1).max(30).optional(), + enableAutoRenewal: z.boolean().optional() + }) + .refine((data) => !(data.renewBeforeDays !== undefined && data.enableAutoRenewal === false), { + message: "Cannot specify both renewBeforeDays and enableAutoRenewal=false" + }), + response: { + 200: z.object({ + message: z.string(), + renewBeforeDays: z.number().optional() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + if (req.body.enableAutoRenewal === false) { + const data = await server.services.certificateV3.disableRenewalConfig({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + certificateId: req.params.id + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG, + metadata: { + certificateId: req.params.id, + commonName: data.commonName + } + } + }); + + return { + message: "Auto-renewal disabled successfully" + }; + } + + if (req.body.renewBeforeDays !== undefined) { + const data = await server.services.certificateV3.updateRenewalConfig({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + certificateId: req.params.id, + renewBeforeDays: req.body.renewBeforeDays + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG, + metadata: { + certificateId: req.params.id, + renewBeforeDays: req.body.renewBeforeDays.toString(), + commonName: data.commonName + } + } + }); + + return { + message: "Certificate configuration updated successfully", + renewBeforeDays: data.renewBeforeDays + }; + } + + return { + message: "No configuration changes requested" + }; + } + }); -export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:serialNumber", + url: "/:id", config: { rateLimit: readLimit }, @@ -31,7 +517,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { tags: [ApiDocsTags.PkiCertificates], description: "Get certificate", params: z.object({ - serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) + id: z.string().trim().describe(CERTIFICATES.GET.id) }), response: { 200: z.object({ @@ -41,7 +527,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const { cert } = await server.services.certificate.getCert({ - serialNumber: req.params.serialNumber, + id: req.params.id, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -67,10 +553,9 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); - // TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best. server.route({ method: "GET", - url: "/:serialNumber/private-key", + url: "/:id/private-key", config: { rateLimit: readLimit }, @@ -80,7 +565,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { tags: [ApiDocsTags.PkiCertificates], description: "Get certificate private key", params: z.object({ - serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) + id: z.string().trim().describe(CERTIFICATES.GET.id) }), response: { 200: z.string().trim() @@ -88,7 +573,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, handler: async (req, reply) => { const { cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({ - serialNumber: req.params.serialNumber, + id: req.params.id, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -114,10 +599,9 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); - // TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best. server.route({ method: "GET", - url: "/:serialNumber/bundle", + url: "/:id/bundle", config: { rateLimit: readLimit }, @@ -127,7 +611,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { tags: [ApiDocsTags.PkiCertificates], description: "Get certificate bundle including the certificate, chain, and private key.", params: z.object({ - serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) + id: z.string().trim().describe(CERTIFICATES.GET_CERT.id) }), response: { 200: z.object({ @@ -141,7 +625,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { handler: async (req, reply) => { const { certificate, certificateChain, serialNumber, cert, privateKey } = await server.services.certificate.getCertBundle({ - serialNumber: req.params.serialNumber, + id: req.params.id, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -172,120 +656,6 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { } }); - server.route({ - method: "POST", - url: "/issue-certificate", - config: { - rateLimit: writeLimit - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - schema: { - hide: false, - tags: [ApiDocsTags.PkiCertificates], - description: "Issue certificate", - body: z - .object({ - caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId), - certificateTemplateId: z - .string() - .trim() - .optional() - .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId), - pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId), - friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName), - commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName), - altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames), - ttl: z - .string() - .refine((val) => ms(val) > 0, "TTL must be a positive number") - .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl), - notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore), - notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter), - keyUsages: z - .nativeEnum(CertKeyUsage) - .array() - .optional() - .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.keyUsages), - extendedKeyUsages: z - .nativeEnum(CertExtendedKeyUsage) - .array() - .optional() - .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.extendedKeyUsages) - }) - .refine( - (data) => { - const { ttl, notAfter } = data; - return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined); - }, - { - message: "Either ttl or notAfter must be present, but not both", - path: ["ttl", "notAfter"] - } - ) - .refine( - (data) => - (data.caId !== undefined && data.certificateTemplateId === undefined) || - (data.caId === undefined && data.certificateTemplateId !== undefined), - { - message: "Either CA ID or Certificate Template ID must be present, but not both", - path: ["caId", "certificateTemplateId"] - } - ), - response: { - 200: z.object({ - certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate), - issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate), - certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain), - privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey), - serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber) - }) - } - }, - handler: async (req) => { - const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = - await server.services.internalCertificateAuthority.issueCertFromCa({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.body - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: ca.projectId, - event: { - type: EventType.ISSUE_CERT, - metadata: { - caId: ca.id, - dn: ca.dn, - serialNumber - } - } - }); - - await server.services.telemetry.sendPostHogEvents({ - event: PostHogEventTypes.IssueCert, - distinctId: getTelemetryDistinctId(req), - organizationId: req.permission.orgId, - properties: { - caId: req.body.caId, - certificateTemplateId: req.body.certificateTemplateId, - commonName: req.body.commonName, - ...req.auditLogInfo - } - }); - - return { - certificate, - certificateChain, - issuingCaCertificate, - privateKey, - serialNumber - }; - } - }); - server.route({ method: "POST", url: "/import-certificate", @@ -350,121 +720,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/sign-certificate", - config: { - rateLimit: writeLimit - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - schema: { - hide: false, - tags: [ApiDocsTags.PkiCertificates], - description: "Sign certificate", - body: z - .object({ - caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId), - certificateTemplateId: z - .string() - .trim() - .optional() - .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId), - pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId), - csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr), - friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName), - commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName), - altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames), - ttl: z - .string() - .refine((val) => ms(val) > 0, "TTL must be a positive number") - .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl), - notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore), - notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter), - keyUsages: z - .nativeEnum(CertKeyUsage) - .array() - .optional() - .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.keyUsages), - extendedKeyUsages: z - .nativeEnum(CertExtendedKeyUsage) - .array() - .optional() - .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.extendedKeyUsages) - }) - .refine( - (data) => { - const { ttl, notAfter } = data; - return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined); - }, - { - message: "Either ttl or notAfter must be present, but not both", - path: ["ttl", "notAfter"] - } - ) - .refine( - (data) => - (data.caId !== undefined && data.certificateTemplateId === undefined) || - (data.caId === undefined && data.certificateTemplateId !== undefined), - { - message: "Either CA ID or Certificate Template ID must be present, but not both", - path: ["caId", "certificateTemplateId"] - } - ), - response: { - 200: z.object({ - certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate), - issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate), - certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain), - serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber) - }) - } - }, - handler: async (req) => { - const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } = - await server.services.internalCertificateAuthority.signCertFromCa({ - isInternal: false, - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.body - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: ca.projectId, - event: { - type: EventType.SIGN_CERT, - metadata: { - caId: ca.id, - dn: ca.dn, - serialNumber - } - } - }); - - await server.services.telemetry.sendPostHogEvents({ - event: PostHogEventTypes.SignCert, - distinctId: getTelemetryDistinctId(req), - organizationId: req.permission.orgId, - properties: { - caId: req.body.caId, - certificateTemplateId: req.body.certificateTemplateId, - commonName, - ...req.auditLogInfo - } - }); - - return { - certificate: certificate.toString("pem"), - certificateChain, - issuingCaCertificate, - serialNumber - }; - } - }); - - server.route({ - method: "POST", - url: "/:serialNumber/revoke", + url: "/:id/revoke", config: { rateLimit: writeLimit }, @@ -474,7 +730,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { tags: [ApiDocsTags.PkiCertificates], description: "Revoke", params: z.object({ - serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumber) + id: z.string().trim().describe(CERTIFICATES.REVOKE.id) }), body: z.object({ revocationReason: z.nativeEnum(CrlReason).describe(CERTIFICATES.REVOKE.revocationReason) @@ -489,7 +745,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const { revokedAt, cert, ca } = await server.services.certificate.revokeCert({ - serialNumber: req.params.serialNumber, + id: req.params.id, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -512,7 +768,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { return { message: "Successfully revoked certificate", - serialNumber: req.params.serialNumber, + serialNumber: cert.serialNumber, revokedAt }; } @@ -520,7 +776,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", - url: "/:serialNumber", + url: "/:id", config: { rateLimit: writeLimit }, @@ -530,7 +786,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { tags: [ApiDocsTags.PkiCertificates], description: "Delete certificate", params: z.object({ - serialNumber: z.string().trim().describe(CERTIFICATES.DELETE.serialNumber) + id: z.string().trim().describe(CERTIFICATES.DELETE.id) }), response: { 200: z.object({ @@ -540,7 +796,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const { deletedCert } = await server.services.certificate.deleteCert({ - serialNumber: req.params.serialNumber, + id: req.params.id, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -568,7 +824,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:serialNumber/certificate", + url: "/:id/certificate", config: { rateLimit: readLimit }, @@ -578,7 +834,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { tags: [ApiDocsTags.PkiCertificates], description: "Get certificate body of certificate", params: z.object({ - serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) + id: z.string().trim().describe(CERTIFICATES.GET_CERT.id) }), response: { 200: z.object({ @@ -590,7 +846,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const { certificate, certificateChain, serialNumber, cert } = await server.services.certificate.getCertBody({ - serialNumber: req.params.serialNumber, + id: req.params.id, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -620,7 +876,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:serialNumber/pkcs12", + url: "/:id/pkcs12", config: { rateLimit: writeLimit }, @@ -630,7 +886,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { tags: [ApiDocsTags.PkiCertificates], description: "Download certificate in PKCS12 format", params: z.object({ - serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) + id: z.string().trim().describe(CERTIFICATES.GET.id) }), body: z.object({ password: z @@ -645,7 +901,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { }, handler: async (req, reply) => { const { pkcs12Data, cert } = await server.services.certificate.getCertPkcs12({ - serialNumber: req.params.serialNumber, + id: req.params.id, password: req.body.password, alias: req.body.alias, actor: req.permission.type, @@ -671,7 +927,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { reply.header("Content-Type", "application/octet-stream"); reply.header( "Content-Disposition", - `attachment; filename="certificate-${req.params.serialNumber.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"` + `attachment; filename="certificate-${cert.serialNumber?.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"` ); return pkcs12Data; diff --git a/backend/src/server/routes/v1/certificate-template-router.ts b/backend/src/server/routes/v1/certificate-template-router.ts index 5ff0e39c0..499d0b98e 100644 --- a/backend/src/server/routes/v1/certificate-template-router.ts +++ b/backend/src/server/routes/v1/certificate-template-router.ts @@ -1,28 +1,239 @@ +import RE2 from "re2"; import { z } from "zod"; -import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { ApiDocsTags, CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; -import { ms } from "@app/lib/ms"; +import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; -import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; -import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema"; -import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; +import { + CertExtendedKeyUsageType, + CertKeyUsageType, + CertSubjectAlternativeNameType, + CertSubjectAttributeType +} from "@app/services/certificate-common/certificate-constants"; +import { certificateTemplateV2ResponseSchema } from "@app/services/certificate-template-v2/certificate-template-v2-schemas"; -const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({ - id: true, - certificateTemplateId: true, - isEnabled: true, - disableBootstrapCertValidation: true +const attributeTypeSchema = z.nativeEnum(CertSubjectAttributeType); +const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType); + +const templateV2SubjectSchema = z + .object({ + type: attributeTypeSchema, + allowed: z.array(z.string()).optional(), + required: z.array(z.string()).optional(), + denied: z.array(z.string()).optional() + }) + .refine( + (data) => { + if (!data.allowed && !data.required && !data.denied) { + return false; + } + return true; + }, + { + message: "Subject attribute must have at least one allowed, required, or denied value" + } + ); + +const templateV2KeyUsagesSchema = z + .object({ + allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(), + required: z.array(z.nativeEnum(CertKeyUsageType)).optional(), + denied: z.array(z.nativeEnum(CertKeyUsageType)).optional() + }) + .refine( + (data) => { + if (!data.allowed && !data.required && !data.denied) { + return false; + } + return true; + }, + { + message: "Key usages must have at least one allowed, required, or denied value" + } + ); + +const templateV2ExtendedKeyUsagesSchema = z + .object({ + allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(), + required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(), + denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional() + }) + .refine( + (data) => { + if (!data.allowed && !data.required && !data.denied) { + return false; + } + return true; + }, + { + message: "Extended key usages must have at least one allowed, required, or denied value" + } + ); + +const templateV2SanSchema = z + .object({ + type: sanTypeSchema, + allowed: z.array(z.string()).optional(), + required: z.array(z.string()).optional(), + denied: z.array(z.string()).optional() + }) + .refine( + (data) => { + if (!data.allowed && !data.required && !data.denied) { + return false; + } + return true; + }, + { + message: "SAN must have at least one allowed, required, or denied value" + } + ); + +const templateV2ValiditySchema = z.object({ + max: z + .string() + .refine( + (val) => { + if (!val) return true; + if (val.length < 2) return false; + const unit = val.slice(-1); + const number = val.slice(0, -1); + const digitRegex = new RE2("^\\d+$"); + return ["d", "h", "m", "y"].includes(unit) && digitRegex.test(number); + }, + { + message: "Max validity must be in format like '365d', '12m', '1y', or '24h'" + } + ) + .optional() +}); + +const templateV2AlgorithmsSchema = z.object({ + signature: z.array(z.string()).min(1, "At least one signature algorithm must be provided").optional(), + keyAlgorithm: z.array(z.string()).min(1, "At least one key algorithm must be provided").optional() +}); + +const createCertificateTemplateV2Schema = z.object({ + projectId: z.string().min(1), + name: z.string().min(1).max(255, "Name must be between 1 and 255 characters"), + description: z.string().max(1000).optional(), + subject: z.array(templateV2SubjectSchema).optional(), + sans: z.array(templateV2SanSchema).optional(), + keyUsages: templateV2KeyUsagesSchema.optional(), + extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(), + algorithms: templateV2AlgorithmsSchema.optional(), + validity: templateV2ValiditySchema.optional() +}); + +const updateCertificateTemplateV2Schema = z.object({ + name: z.string().min(1).max(255, "Name must be between 1 and 255 characters").optional(), + description: z.string().max(1000).optional(), + subject: z.array(templateV2SubjectSchema).optional(), + sans: z.array(templateV2SanSchema).optional(), + keyUsages: templateV2KeyUsagesSchema.optional(), + extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(), + algorithms: templateV2AlgorithmsSchema.optional(), + validity: templateV2ValiditySchema.optional() }); export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], + body: createCertificateTemplateV2Schema, + response: { + 200: z.object({ + certificateTemplate: certificateTemplateV2ResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { projectId, ...data } = req.body; + const certificateTemplate = await server.services.certificateTemplateV2.createTemplateV2({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod!, + actorOrgId: req.permission.orgId, + projectId, + data + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.CREATE_CERTIFICATE_TEMPLATE, + metadata: { + certificateTemplateId: certificateTemplate.id, + name: certificateTemplate.name, + projectId: certificateTemplate.projectId + } + } + }); + + return { certificateTemplate }; + } + }); + server.route({ method: "GET", - url: "/:certificateTemplateId", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], + querystring: z.object({ + projectId: z.string().min(1), + offset: z.coerce.number().min(0).default(0), + limit: z.coerce.number().min(1).max(100).default(20), + search: z.string().optional() + }), + response: { + 200: z.object({ + certificateTemplates: certificateTemplateV2ResponseSchema.array(), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { templates, totalCount } = await server.services.certificateTemplateV2.listTemplatesV2({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod!, + actorOrgId: req.permission.orgId, + ...req.query + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.LIST_CERTIFICATE_TEMPLATES, + metadata: { + projectId: req.query.projectId + } + } + }); + + return { certificateTemplates: templates, totalCount }; + } + }); + + server.route({ + method: "GET", + url: "/:id", config: { rateLimit: readLimit }, @@ -30,20 +241,22 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid hide: false, tags: [ApiDocsTags.PkiCertificateTemplates], params: z.object({ - certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.GET.certificateTemplateId) + id: z.string().uuid() }), response: { - 200: sanitizedCertificateTemplate + 200: z.object({ + certificateTemplate: certificateTemplateV2ResponseSchema + }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const certificateTemplate = await server.services.certificateTemplate.getCertTemplate({ - id: req.params.certificateTemplateId, + const certificateTemplate = await server.services.certificateTemplateV2.getTemplateV2ById({ actor: req.permission.type, actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + actorAuthMethod: req.permission.authMethod!, + actorOrgId: req.permission.orgId, + templateId: req.params.id }); await server.services.auditLog.createAuditLog({ @@ -58,125 +271,38 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid } }); - return certificateTemplate; - } - }); - - server.route({ - method: "POST", - url: "/", - config: { - rateLimit: writeLimit - }, - schema: { - hide: false, - tags: [ApiDocsTags.PkiCertificateTemplates], - body: z.object({ - caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId), - pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId), - name: slugSchema().describe(CERTIFICATE_TEMPLATES.CREATE.name), - commonName: validateTemplateRegexField.describe(CERTIFICATE_TEMPLATES.CREATE.commonName), - subjectAlternativeName: validateTemplateRegexField.describe( - CERTIFICATE_TEMPLATES.CREATE.subjectAlternativeName - ), - ttl: z - .string() - .refine((val) => ms(val) > 0, "TTL must be a positive number") - .describe(CERTIFICATE_TEMPLATES.CREATE.ttl), - keyUsages: z - .nativeEnum(CertKeyUsage) - .array() - .optional() - .default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]) - .describe(CERTIFICATE_TEMPLATES.CREATE.keyUsages), - extendedKeyUsages: z - .nativeEnum(CertExtendedKeyUsage) - .array() - .optional() - .default([]) - .describe(CERTIFICATE_TEMPLATES.CREATE.extendedKeyUsages) - }), - response: { - 200: sanitizedCertificateTemplate - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const certificateTemplate = await server.services.certificateTemplate.createCertTemplate({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.body - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: certificateTemplate.projectId, - event: { - type: EventType.CREATE_CERTIFICATE_TEMPLATE, - metadata: { - certificateTemplateId: certificateTemplate.id, - caId: certificateTemplate.caId, - pkiCollectionId: certificateTemplate.pkiCollectionId as string, - name: certificateTemplate.name, - commonName: certificateTemplate.commonName, - subjectAlternativeName: certificateTemplate.subjectAlternativeName, - ttl: certificateTemplate.ttl, - projectId: certificateTemplate.projectId - } - } - }); - - return certificateTemplate; + return { certificateTemplate }; } }); server.route({ method: "PATCH", - url: "/:certificateTemplateId", + url: "/:id", config: { rateLimit: writeLimit }, schema: { hide: false, tags: [ApiDocsTags.PkiCertificateTemplates], - body: z.object({ - caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId), - pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId), - name: slugSchema().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.name), - commonName: validateTemplateRegexField.optional().describe(CERTIFICATE_TEMPLATES.UPDATE.commonName), - subjectAlternativeName: validateTemplateRegexField - .optional() - .describe(CERTIFICATE_TEMPLATES.UPDATE.subjectAlternativeName), - ttl: z - .string() - .refine((val) => ms(val) > 0, "TTL must be a positive number") - .optional() - .describe(CERTIFICATE_TEMPLATES.UPDATE.ttl), - keyUsages: z.nativeEnum(CertKeyUsage).array().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.keyUsages), - extendedKeyUsages: z - .nativeEnum(CertExtendedKeyUsage) - .array() - .optional() - .describe(CERTIFICATE_TEMPLATES.UPDATE.extendedKeyUsages) - }), params: z.object({ - certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId) + id: z.string().uuid() }), + body: updateCertificateTemplateV2Schema, response: { - 200: sanitizedCertificateTemplate + 200: z.object({ + certificateTemplate: certificateTemplateV2ResponseSchema + }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const certificateTemplate = await server.services.certificateTemplate.updateCertTemplate({ - ...req.body, - id: req.params.certificateTemplateId, + const certificateTemplate = await server.services.certificateTemplateV2.updateTemplateV2({ actor: req.permission.type, actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + actorAuthMethod: req.permission.authMethod!, + actorOrgId: req.permission.orgId, + templateId: req.params.id, + data: req.body }); await server.services.auditLog.createAuditLog({ @@ -186,23 +312,18 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid type: EventType.UPDATE_CERTIFICATE_TEMPLATE, metadata: { certificateTemplateId: certificateTemplate.id, - name: certificateTemplate.name, - caId: certificateTemplate.caId, - pkiCollectionId: certificateTemplate.pkiCollectionId as string, - commonName: certificateTemplate.commonName, - subjectAlternativeName: certificateTemplate.subjectAlternativeName, - ttl: certificateTemplate.ttl + name: certificateTemplate.name } } }); - return certificateTemplate; + return { certificateTemplate }; } }); server.route({ method: "DELETE", - url: "/:certificateTemplateId", + url: "/:id", config: { rateLimit: writeLimit }, @@ -210,20 +331,22 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid hide: false, tags: [ApiDocsTags.PkiCertificateTemplates], params: z.object({ - certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId) + id: z.string().uuid() }), response: { - 200: sanitizedCertificateTemplate + 200: z.object({ + certificateTemplate: certificateTemplateV2ResponseSchema + }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const certificateTemplate = await server.services.certificateTemplate.deleteCertTemplate({ - id: req.params.certificateTemplateId, + const certificateTemplate = await server.services.certificateTemplateV2.deleteTemplateV2({ actor: req.permission.type, actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + actorAuthMethod: req.permission.authMethod!, + actorOrgId: req.permission.orgId, + templateId: req.params.id }); await server.services.auditLog.createAuditLog({ @@ -238,158 +361,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid } }); - return certificateTemplate; - } - }); - - server.route({ - method: "POST", - url: "/:certificateTemplateId/est-config", - config: { - rateLimit: writeLimit - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - schema: { - hide: false, - tags: [ApiDocsTags.PkiCertificateTemplates], - description: "Create Certificate Template EST configuration", - params: z.object({ - certificateTemplateId: z.string().trim() - }), - body: z - .object({ - caChain: z.string().trim().optional(), - passphrase: z.string().min(1), - isEnabled: z.boolean().default(true), - disableBootstrapCertValidation: z.boolean().default(false) - }) - .refine( - ({ caChain, disableBootstrapCertValidation }) => - disableBootstrapCertValidation || (!disableBootstrapCertValidation && caChain), - "CA chain is required" - ), - response: { - 200: sanitizedEstConfig - } - }, - handler: async (req) => { - const estConfig = await server.services.certificateTemplate.createEstConfiguration({ - certificateTemplateId: req.params.certificateTemplateId, - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.body - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: estConfig.projectId, - event: { - type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG, - metadata: { - certificateTemplateId: estConfig.certificateTemplateId, - isEnabled: estConfig.isEnabled as boolean - } - } - }); - - return estConfig; - } - }); - - server.route({ - method: "PATCH", - url: "/:certificateTemplateId/est-config", - config: { - rateLimit: writeLimit - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - schema: { - hide: false, - tags: [ApiDocsTags.PkiCertificateTemplates], - description: "Update Certificate Template EST configuration", - params: z.object({ - certificateTemplateId: z.string().trim() - }), - body: z.object({ - caChain: z.string().trim().optional(), - passphrase: z.string().min(1).optional(), - disableBootstrapCertValidation: z.boolean().optional(), - isEnabled: z.boolean().optional() - }), - response: { - 200: sanitizedEstConfig - } - }, - handler: async (req) => { - const estConfig = await server.services.certificateTemplate.updateEstConfiguration({ - certificateTemplateId: req.params.certificateTemplateId, - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.body - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: estConfig.projectId, - event: { - type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG, - metadata: { - certificateTemplateId: estConfig.certificateTemplateId, - isEnabled: estConfig.isEnabled as boolean - } - } - }); - - return estConfig; - } - }); - - server.route({ - method: "GET", - url: "/:certificateTemplateId/est-config", - config: { - rateLimit: readLimit - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - schema: { - hide: false, - tags: [ApiDocsTags.PkiCertificateTemplates], - description: "Get Certificate Template EST configuration", - params: z.object({ - certificateTemplateId: z.string().trim() - }), - response: { - 200: sanitizedEstConfig.extend({ - caChain: z.string() - }) - } - }, - handler: async (req) => { - const estConfig = await server.services.certificateTemplate.getEstConfiguration({ - isInternal: false, - certificateTemplateId: req.params.certificateTemplateId, - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId - }); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: estConfig.projectId, - event: { - type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG, - metadata: { - certificateTemplateId: estConfig.certificateTemplateId - } - } - }); - - return estConfig; + return { certificateTemplate }; } }); }; diff --git a/backend/src/server/routes/v1/deprecated-certificate-authority-routers/acme-certificate-authority-router.ts b/backend/src/server/routes/v1/deprecated-certificate-authority-routers/acme-certificate-authority-router.ts new file mode 100644 index 000000000..3c549ac1b --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-certificate-authority-routers/acme-certificate-authority-router.ts @@ -0,0 +1,18 @@ +import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas"; +import { + CreateAcmeCertificateAuthoritySchema, + UpdateAcmeCertificateAuthoritySchema +} from "@app/services/certificate-authority/acme/deprecated-acme-certificate-authority-schemas"; +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; + +import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints"; + +export const registerAcmeCertificateAuthorityRouter = async (server: FastifyZodProvider) => { + registerCertificateAuthorityEndpoints({ + caType: CaType.ACME, + server, + responseSchema: AcmeCertificateAuthoritySchema, + createSchema: CreateAcmeCertificateAuthoritySchema, + updateSchema: UpdateAcmeCertificateAuthoritySchema + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-certificate-authority-routers/azure-ad-cs-certificate-authority-router.ts b/backend/src/server/routes/v1/deprecated-certificate-authority-routers/azure-ad-cs-certificate-authority-router.ts new file mode 100644 index 000000000..9407ee681 --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-certificate-authority-routers/azure-ad-cs-certificate-authority-router.ts @@ -0,0 +1,78 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { AzureAdCsCertificateAuthoritySchema } from "@app/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas"; +import { + CreateAzureAdCsCertificateAuthoritySchema, + UpdateAzureAdCsCertificateAuthoritySchema +} from "@app/services/certificate-authority/azure-ad-cs/deprecated-azure-ad-cs-certificate-authority-schemas"; +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; + +import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints"; + +export const registerAzureAdCsCertificateAuthorityRouter = async (server: FastifyZodProvider) => { + registerCertificateAuthorityEndpoints({ + caType: CaType.AZURE_AD_CS, + server, + responseSchema: AzureAdCsCertificateAuthoritySchema, + createSchema: CreateAzureAdCsCertificateAuthoritySchema, + updateSchema: UpdateAzureAdCsCertificateAuthoritySchema + }); + + server.route({ + method: "GET", + url: "/:caId/templates", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + description: "Get available certificate templates from Azure AD CS CA", + params: z.object({ + caId: z.string().describe("Azure AD CS CA ID") + }), + querystring: z.object({ + projectId: z.string().describe("Project ID") + }), + response: { + 200: z.object({ + templates: z.array( + z.object({ + id: z.string().describe("Template identifier"), + name: z.string().describe("Template display name"), + description: z.string().optional().describe("Template description") + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const templates = await server.services.certificateAuthority.getAzureAdcsTemplates({ + caId: req.params.caId, + projectId: req.query.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.GET_AZURE_AD_TEMPLATES, + metadata: { + caId: req.params.caId, + amount: templates.length + } + } + }); + + return { templates }; + } + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-certificate-authority-routers/certificate-authority-endpoints.ts b/backend/src/server/routes/v1/deprecated-certificate-authority-routers/certificate-authority-endpoints.ts new file mode 100644 index 000000000..dd0f8b215 --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-certificate-authority-routers/certificate-authority-endpoints.ts @@ -0,0 +1,258 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { + TCertificateAuthority, + TCertificateAuthorityInput +} from "@app/services/certificate-authority/certificate-authority-types"; + +export const registerCertificateAuthorityEndpoints = < + T extends TCertificateAuthority, + I extends TCertificateAuthorityInput +>({ + server, + caType, + createSchema, + updateSchema, + responseSchema +}: { + caType: CaType; + server: FastifyZodProvider; + createSchema: z.ZodType<{ + name: string; + projectId: string; + status: CaStatus; + configuration: I["configuration"]; + enableDirectIssuance: boolean; + }>; + updateSchema: z.ZodType<{ + projectId: string; + name?: string; + status?: CaStatus; + configuration?: I["configuration"]; + enableDirectIssuance?: boolean; + }>; + responseSchema: z.ZodTypeAny; +}) => { + server.route({ + method: "GET", + url: `/`, + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + querystring: z.object({ + projectId: z.string().trim().min(1, "Project ID required") + }), + response: { + 200: responseSchema.array() + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + query: { projectId } + } = req; + + const certificateAuthorities = (await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId( + { projectId, type: caType }, + req.permission + )) as T[]; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_CAS, + metadata: { + caIds: certificateAuthorities.map((ca) => ca.id) + } + } + }); + + return certificateAuthorities; + } + }); + + server.route({ + method: "GET", + url: "/:caName", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + params: z.object({ + caName: z.string() + }), + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: responseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { caName } = req.params; + const { projectId } = req.query; + + const certificateAuthority = + (await server.services.certificateAuthority.findCertificateAuthorityByNameAndProjectId( + { caName, type: caType, projectId }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: certificateAuthority.projectId, + event: { + type: EventType.GET_CA, + metadata: { + caId: certificateAuthority.id, + name: certificateAuthority.name + } + } + }); + + return certificateAuthority; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + body: createSchema, + response: { + 200: responseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateAuthority = (await server.services.certificateAuthority.createCertificateAuthority( + { ...req.body, type: caType }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: certificateAuthority.projectId, + event: { + type: EventType.CREATE_CA, + metadata: { + name: certificateAuthority.name, + caId: certificateAuthority.id + } + } + }); + + return certificateAuthority; + } + }); + + server.route({ + method: "PATCH", + url: "/:caName", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + params: z.object({ + caName: z.string() + }), + body: updateSchema, + response: { + 200: responseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { caName } = req.params; + + const certificateAuthority = (await server.services.certificateAuthority.deprecatedUpdateCertificateAuthority( + { + ...req.body, + type: caType, + caName + }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: certificateAuthority.projectId, + event: { + type: EventType.UPDATE_CA, + metadata: { + name: certificateAuthority.name, + caId: certificateAuthority.id, + status: certificateAuthority.status + } + } + }); + + return certificateAuthority; + } + }); + + server.route({ + method: "DELETE", + url: "/:caName", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + params: z.object({ + caName: z.string() + }), + body: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: responseSchema + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { caName } = req.params; + const { projectId } = req.body; + + const certificateAuthority = (await server.services.certificateAuthority.deprecatedDeleteCertificateAuthority( + { caName, type: caType, projectId }, + req.permission + )) as T; + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: certificateAuthority.projectId, + event: { + type: EventType.DELETE_CA, + metadata: { + name: certificateAuthority.name, + caId: certificateAuthority.id + } + } + }); + + return certificateAuthority; + } + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-certificate-authority-routers/index.ts b/backend/src/server/routes/v1/deprecated-certificate-authority-routers/index.ts new file mode 100644 index 000000000..69a783620 --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-certificate-authority-routers/index.ts @@ -0,0 +1,16 @@ +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; + +import { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router"; +import { registerAzureAdCsCertificateAuthorityRouter } from "./azure-ad-cs-certificate-authority-router"; +import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router"; + +export * from "./internal-certificate-authority-router"; + +export const DEPRECATED_CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record< + CaType, + (server: FastifyZodProvider) => Promise +> = { + [CaType.INTERNAL]: registerInternalCertificateAuthorityRouter, + [CaType.ACME]: registerAcmeCertificateAuthorityRouter, + [CaType.AZURE_AD_CS]: registerAzureAdCsCertificateAuthorityRouter +}; diff --git a/backend/src/server/routes/v1/deprecated-certificate-authority-routers/internal-certificate-authority-router.ts b/backend/src/server/routes/v1/deprecated-certificate-authority-routers/internal-certificate-authority-router.ts new file mode 100644 index 000000000..848367d70 --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-certificate-authority-routers/internal-certificate-authority-router.ts @@ -0,0 +1,18 @@ +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { + CreateInternalCertificateAuthoritySchema, + UpdateInternalCertificateAuthoritySchema +} from "@app/services/certificate-authority/internal/deprecated-internal-certificate-authority-schemas"; +import { InternalCertificateAuthoritySchema } from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas"; + +import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints"; + +export const registerInternalCertificateAuthorityRouter = async (server: FastifyZodProvider) => { + registerCertificateAuthorityEndpoints({ + caType: CaType.INTERNAL, + server, + responseSchema: InternalCertificateAuthoritySchema, + createSchema: CreateInternalCertificateAuthoritySchema, + updateSchema: UpdateInternalCertificateAuthoritySchema + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-certificate-router.ts b/backend/src/server/routes/v1/deprecated-certificate-router.ts new file mode 100644 index 000000000..955407e4c --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-certificate-router.ts @@ -0,0 +1,680 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import RE2 from "re2"; +import { z } from "zod"; + +import { CertificatesSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { addNoCacheHeaders } from "@app/server/lib/caching"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types"; +import { + validateAltNamesField, + validateCaDateField +} from "@app/services/certificate-authority/certificate-authority-validators"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + +export const registerDeprecatedCertRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:serialNumber", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Get certificate", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) + }), + response: { + 200: z.object({ + certificate: CertificatesSchema + }) + } + }, + handler: async (req) => { + const { cert } = await server.services.certificate.getCert({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: cert.projectId, + event: { + type: EventType.GET_CERT, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + return { + certificate: cert + }; + } + }); + + // TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best. + server.route({ + method: "GET", + url: "/:serialNumber/private-key", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Get certificate private key", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) + }), + response: { + 200: z.string().trim() + } + }, + handler: async (req, reply) => { + const { cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: cert.projectId, + event: { + type: EventType.GET_CERT_PRIVATE_KEY, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + addNoCacheHeaders(reply); + + return certPrivateKey; + } + }); + + // TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best. + server.route({ + method: "GET", + url: "/:serialNumber/bundle", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Get certificate bundle including the certificate, chain, and private key.", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), + certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain), + privateKey: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.privateKey), + serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) + }) + } + }, + handler: async (req, reply) => { + const { certificate, certificateChain, serialNumber, cert, privateKey } = + await server.services.certificate.getCertBundle({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: cert.projectId, + event: { + type: EventType.GET_CERT_BUNDLE, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + addNoCacheHeaders(reply); + + return { + certificate, + certificateChain, + serialNumber, + privateKey + }; + } + }); + + server.route({ + method: "POST", + url: "/issue-certificate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Issue certificate", + body: z + .object({ + caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId), + certificateTemplateId: z + .string() + .trim() + .optional() + .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId), + pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId), + friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName), + commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName), + altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames), + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl), + notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore), + notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter), + keyUsages: z + .nativeEnum(CertKeyUsage) + .array() + .optional() + .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .optional() + .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.extendedKeyUsages) + }) + .refine( + (data) => { + const { ttl, notAfter } = data; + return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined); + }, + { + message: "Either ttl or notAfter must be present, but not both", + path: ["ttl", "notAfter"] + } + ) + .refine( + (data) => + (data.caId !== undefined && data.certificateTemplateId === undefined) || + (data.caId === undefined && data.certificateTemplateId !== undefined), + { + message: "Either CA ID or Certificate Template ID must be present, but not both", + path: ["caId", "certificateTemplateId"] + } + ), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate), + issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate), + certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain), + privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey), + serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } = + await server.services.internalCertificateAuthority.issueCertFromCa({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.ISSUE_CERT, + metadata: { + caId: ca.id, + dn: ca.dn, + serialNumber + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IssueCert, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + caId: req.body.caId, + certificateTemplateId: req.body.certificateTemplateId, + commonName: req.body.commonName, + ...req.auditLogInfo + } + }); + + return { + certificate, + certificateChain, + issuingCaCertificate, + privateKey, + serialNumber + }; + } + }); + + server.route({ + method: "POST", + url: "/import-certificate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Import certificate", + body: z.object({ + projectSlug: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.projectSlug), + + certificatePem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.certificatePem), + privateKeyPem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.privateKeyPem), + chainPem: z.string().trim().min(1).describe(CERTIFICATES.IMPORT.chainPem), + + friendlyName: z.string().trim().optional().describe(CERTIFICATES.IMPORT.friendlyName), + pkiCollectionId: z.string().trim().optional().describe(CERTIFICATES.IMPORT.pkiCollectionId) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATES.IMPORT.certificate), + certificateChain: z.string().trim().describe(CERTIFICATES.IMPORT.certificateChain), + privateKey: z.string().trim().describe(CERTIFICATES.IMPORT.privateKey), + serialNumber: z.string().trim().describe(CERTIFICATES.IMPORT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, privateKey, serialNumber, cert } = + await server.services.certificate.importCert({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: cert.projectId, + event: { + type: EventType.IMPORT_CERT, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber + } + } + }); + + return { + certificate, + certificateChain, + privateKey, + serialNumber + }; + } + }); + + server.route({ + method: "POST", + url: "/sign-certificate", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Sign certificate", + body: z + .object({ + caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId), + certificateTemplateId: z + .string() + .trim() + .optional() + .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId), + pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId), + csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr), + friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName), + commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName), + altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames), + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl), + notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore), + notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter), + keyUsages: z + .nativeEnum(CertKeyUsage) + .array() + .optional() + .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .optional() + .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.extendedKeyUsages) + }) + .refine( + (data) => { + const { ttl, notAfter } = data; + return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined); + }, + { + message: "Either ttl or notAfter must be present, but not both", + path: ["ttl", "notAfter"] + } + ) + .refine( + (data) => + (data.caId !== undefined && data.certificateTemplateId === undefined) || + (data.caId === undefined && data.certificateTemplateId !== undefined), + { + message: "Either CA ID or Certificate Template ID must be present, but not both", + path: ["caId", "certificateTemplateId"] + } + ), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate), + issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate), + certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain), + serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } = + await server.services.internalCertificateAuthority.signCertFromCa({ + isInternal: false, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.SIGN_CERT, + metadata: { + caId: ca.id, + dn: ca.dn, + serialNumber + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SignCert, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + caId: req.body.caId, + certificateTemplateId: req.body.certificateTemplateId, + commonName, + ...req.auditLogInfo + } + }); + + return { + certificate: certificate.toString("pem"), + certificateChain, + issuingCaCertificate, + serialNumber + }; + } + }); + + server.route({ + method: "POST", + url: "/:serialNumber/revoke", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Revoke", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumber) + }), + body: z.object({ + revocationReason: z.nativeEnum(CrlReason).describe(CERTIFICATES.REVOKE.revocationReason) + }), + response: { + 200: z.object({ + message: z.string().trim(), + serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumberRes), + revokedAt: z.date().describe(CERTIFICATES.REVOKE.revokedAt) + }) + } + }, + handler: async (req) => { + const { revokedAt, cert, ca } = await server.services.certificate.revokeCert({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.REVOKE_CERT, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + return { + message: "Successfully revoked certificate", + serialNumber: req.params.serialNumber, + revokedAt + }; + } + }); + + server.route({ + method: "DELETE", + url: "/:serialNumber", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Delete certificate", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.DELETE.serialNumber) + }), + response: { + 200: z.object({ + certificate: CertificatesSchema + }) + } + }, + handler: async (req) => { + const { deletedCert } = await server.services.certificate.deleteCert({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: deletedCert.projectId, + event: { + type: EventType.DELETE_CERT, + metadata: { + certId: deletedCert.id, + cn: deletedCert.commonName, + serialNumber: deletedCert.serialNumber + } + } + }); + + return { + certificate: deletedCert + }; + } + }); + + server.route({ + method: "GET", + url: "/:serialNumber/certificate", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + description: "Get certificate body of certificate", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) + }), + response: { + 200: z.object({ + certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), + certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain), + serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) + }) + } + }, + handler: async (req) => { + const { certificate, certificateChain, serialNumber, cert } = await server.services.certificate.getCertBody({ + serialNumber: req.params.serialNumber, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: cert.projectId, + event: { + type: EventType.GET_CERT_BODY, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + return { + certificate, + certificateChain, + serialNumber + }; + } + }); + + server.route({ + method: "POST", + url: "/:serialNumber/pkcs12", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + hide: true, + tags: [ApiDocsTags.PkiCertificates], + description: "Download certificate in PKCS12 format", + params: z.object({ + serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) + }), + body: z.object({ + password: z + .string() + .min(6, "Password must be at least 6 characters long") + .describe("Password for the keystore (minimum 6 characters)"), + alias: z.string().min(1, "Alias is required").describe("Alias for the certificate in the keystore") + }), + response: { + 200: z.any().describe("PKCS12 keystore as binary data") + } + }, + handler: async (req, reply) => { + const { pkcs12Data, cert } = await server.services.certificate.getCertPkcs12({ + serialNumber: req.params.serialNumber, + password: req.body.password, + alias: req.body.alias, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: cert.projectId, + event: { + type: EventType.EXPORT_CERT_PKCS12, + metadata: { + certId: cert.id, + cn: cert.commonName, + serialNumber: cert.serialNumber + } + } + }); + + addNoCacheHeaders(reply); + reply.header("Content-Type", "application/octet-stream"); + reply.header( + "Content-Disposition", + `attachment; filename="certificate-${req.params.serialNumber.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"` + ); + + return pkcs12Data; + } + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-certificate-template-router.ts b/backend/src/server/routes/v1/deprecated-certificate-template-router.ts new file mode 100644 index 000000000..6737b30c0 --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-certificate-template-router.ts @@ -0,0 +1,395 @@ +import { z } from "zod"; + +import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema"; +import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; + +const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({ + id: true, + certificateTemplateId: true, + isEnabled: true, + disableBootstrapCertValidation: true +}); + +export const registerDeprecatedCertificateTemplateRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/:certificateTemplateId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], + params: z.object({ + certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.GET.certificateTemplateId) + }), + response: { + 200: sanitizedCertificateTemplate + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateTemplate = await server.services.certificateTemplate.getCertTemplate({ + id: req.params.certificateTemplateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: certificateTemplate.projectId, + event: { + type: EventType.GET_CERTIFICATE_TEMPLATE, + metadata: { + certificateTemplateId: certificateTemplate.id, + name: certificateTemplate.name + } + } + }); + + return certificateTemplate; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], + body: z.object({ + caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId), + pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId), + name: slugSchema().describe(CERTIFICATE_TEMPLATES.CREATE.name), + commonName: validateTemplateRegexField.describe(CERTIFICATE_TEMPLATES.CREATE.commonName), + subjectAlternativeName: validateTemplateRegexField.describe( + CERTIFICATE_TEMPLATES.CREATE.subjectAlternativeName + ), + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .describe(CERTIFICATE_TEMPLATES.CREATE.ttl), + keyUsages: z + .nativeEnum(CertKeyUsage) + .array() + .optional() + .default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]) + .describe(CERTIFICATE_TEMPLATES.CREATE.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .optional() + .default([]) + .describe(CERTIFICATE_TEMPLATES.CREATE.extendedKeyUsages) + }), + response: { + 200: sanitizedCertificateTemplate + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateTemplate = await server.services.certificateTemplate.createCertTemplate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: certificateTemplate.projectId, + event: { + type: EventType.CREATE_CERTIFICATE_TEMPLATE, + metadata: { + certificateTemplateId: certificateTemplate.id, + caId: certificateTemplate.caId, + pkiCollectionId: certificateTemplate.pkiCollectionId as string, + name: certificateTemplate.name, + commonName: certificateTemplate.commonName, + subjectAlternativeName: certificateTemplate.subjectAlternativeName, + ttl: certificateTemplate.ttl, + projectId: certificateTemplate.projectId + } + } + }); + + return certificateTemplate; + } + }); + + server.route({ + method: "PATCH", + url: "/:certificateTemplateId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], + body: z.object({ + caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId), + pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId), + name: slugSchema().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.name), + commonName: validateTemplateRegexField.optional().describe(CERTIFICATE_TEMPLATES.UPDATE.commonName), + subjectAlternativeName: validateTemplateRegexField + .optional() + .describe(CERTIFICATE_TEMPLATES.UPDATE.subjectAlternativeName), + ttl: z + .string() + .refine((val) => ms(val) > 0, "TTL must be a positive number") + .optional() + .describe(CERTIFICATE_TEMPLATES.UPDATE.ttl), + keyUsages: z.nativeEnum(CertKeyUsage).array().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.keyUsages), + extendedKeyUsages: z + .nativeEnum(CertExtendedKeyUsage) + .array() + .optional() + .describe(CERTIFICATE_TEMPLATES.UPDATE.extendedKeyUsages) + }), + params: z.object({ + certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId) + }), + response: { + 200: sanitizedCertificateTemplate + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateTemplate = await server.services.certificateTemplate.updateCertTemplate({ + ...req.body, + id: req.params.certificateTemplateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: certificateTemplate.projectId, + event: { + type: EventType.UPDATE_CERTIFICATE_TEMPLATE, + metadata: { + certificateTemplateId: certificateTemplate.id, + name: certificateTemplate.name, + caId: certificateTemplate.caId, + pkiCollectionId: certificateTemplate.pkiCollectionId as string, + commonName: certificateTemplate.commonName, + subjectAlternativeName: certificateTemplate.subjectAlternativeName, + ttl: certificateTemplate.ttl + } + } + }); + + return certificateTemplate; + } + }); + + server.route({ + method: "DELETE", + url: "/:certificateTemplateId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], + params: z.object({ + certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId) + }), + response: { + 200: sanitizedCertificateTemplate + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateTemplate = await server.services.certificateTemplate.deleteCertTemplate({ + id: req.params.certificateTemplateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: certificateTemplate.projectId, + event: { + type: EventType.DELETE_CERTIFICATE_TEMPLATE, + metadata: { + certificateTemplateId: certificateTemplate.id, + name: certificateTemplate.name + } + } + }); + + return certificateTemplate; + } + }); + + server.route({ + method: "POST", + url: "/:certificateTemplateId/est-config", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], + description: "Create Certificate Template EST configuration", + params: z.object({ + certificateTemplateId: z.string().trim() + }), + body: z + .object({ + caChain: z.string().trim().optional(), + passphrase: z.string().min(1), + isEnabled: z.boolean().default(true), + disableBootstrapCertValidation: z.boolean().default(false) + }) + .refine( + ({ caChain, disableBootstrapCertValidation }) => + disableBootstrapCertValidation || (!disableBootstrapCertValidation && caChain), + "CA chain is required" + ), + response: { + 200: sanitizedEstConfig + } + }, + handler: async (req) => { + const estConfig = await server.services.certificateTemplate.createEstConfiguration({ + certificateTemplateId: req.params.certificateTemplateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: estConfig.projectId, + event: { + type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG, + metadata: { + certificateTemplateId: estConfig.certificateTemplateId, + isEnabled: estConfig.isEnabled as boolean + } + } + }); + + return estConfig; + } + }); + + server.route({ + method: "PATCH", + url: "/:certificateTemplateId/est-config", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], + description: "Update Certificate Template EST configuration", + params: z.object({ + certificateTemplateId: z.string().trim() + }), + body: z.object({ + caChain: z.string().trim().optional(), + passphrase: z.string().min(1).optional(), + disableBootstrapCertValidation: z.boolean().optional(), + isEnabled: z.boolean().optional() + }), + response: { + 200: sanitizedEstConfig + } + }, + handler: async (req) => { + const estConfig = await server.services.certificateTemplate.updateEstConfiguration({ + certificateTemplateId: req.params.certificateTemplateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: estConfig.projectId, + event: { + type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG, + metadata: { + certificateTemplateId: estConfig.certificateTemplateId, + isEnabled: estConfig.isEnabled as boolean + } + } + }); + + return estConfig; + } + }); + + server.route({ + method: "GET", + url: "/:certificateTemplateId/est-config", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateTemplates], + description: "Get Certificate Template EST configuration", + params: z.object({ + certificateTemplateId: z.string().trim() + }), + response: { + 200: sanitizedEstConfig.extend({ + caChain: z.string() + }) + } + }, + handler: async (req) => { + const estConfig = await server.services.certificateTemplate.getEstConfiguration({ + isInternal: false, + certificateTemplateId: req.params.certificateTemplateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: estConfig.projectId, + event: { + type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG, + metadata: { + certificateTemplateId: estConfig.certificateTemplateId + } + } + }); + + return estConfig; + } + }); +}; diff --git a/backend/src/server/routes/v1/deprecated-pki-alert-router.ts b/backend/src/server/routes/v1/deprecated-pki-alert-router.ts new file mode 100644 index 000000000..bfabc5a89 --- /dev/null +++ b/backend/src/server/routes/v1/deprecated-pki-alert-router.ts @@ -0,0 +1,205 @@ +import { z } from "zod"; + +import { PkiAlertsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ALERTS, ApiDocsTags } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { PkiAlertEventType } from "@app/services/pki-alert-v2/pki-alert-v2-types"; + +export const registerDeprecatedPkiAlertRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + tags: [ApiDocsTags.PkiAlerting], + description: "Create PKI alert", + body: z.object({ + projectId: z.string().trim().describe(ALERTS.CREATE.projectId), + pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId), + name: z.string().trim().describe(ALERTS.CREATE.name), + alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays), + emails: z + .array(z.string().trim().email({ message: "Invalid email address" })) + .min(1, { message: "You must specify at least 1 email" }) + .max(5, { message: "You can specify a maximum of 5 emails" }) + .describe(ALERTS.CREATE.emails) + }), + response: { + 200: PkiAlertsSchema + } + }, + handler: async (req) => { + const alert = await server.services.pkiAlert.createPkiAlert({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: alert.projectId, + event: { + type: EventType.CREATE_PKI_ALERT, + metadata: { + pkiAlertId: alert.id, + pkiCollectionId: alert.pkiCollectionId, + name: alert.name, + alertBefore: alert.alertBeforeDays.toString(), + eventType: PkiAlertEventType.EXPIRATION, + recipientEmails: alert.recipientEmails + } + } + }); + + return alert; + } + }); + + server.route({ + method: "GET", + url: "/:alertId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + tags: [ApiDocsTags.PkiAlerting], + description: "Get PKI alert", + params: z.object({ + alertId: z.string().trim().describe(ALERTS.GET.alertId) + }), + response: { + 200: PkiAlertsSchema + } + }, + handler: async (req) => { + const alert = await server.services.pkiAlert.getPkiAlertById({ + alertId: req.params.alertId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: alert.projectId, + event: { + type: EventType.GET_PKI_ALERT, + metadata: { + pkiAlertId: alert.id + } + } + }); + + return alert; + } + }); + + server.route({ + method: "PATCH", + url: "/:alertId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + tags: [ApiDocsTags.PkiAlerting], + description: "Update PKI alert", + params: z.object({ + alertId: z.string().trim().describe(ALERTS.UPDATE.alertId) + }), + body: z.object({ + name: z.string().trim().optional().describe(ALERTS.UPDATE.name), + alertBeforeDays: z.number().optional().describe(ALERTS.UPDATE.alertBeforeDays), + pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId), + emails: z + .array(z.string().trim().email({ message: "Invalid email address" })) + .min(1, { message: "You must specify at least 1 email" }) + .max(5, { message: "You can specify a maximum of 5 emails" }) + .optional() + .describe(ALERTS.UPDATE.emails) + }), + response: { + 200: PkiAlertsSchema + } + }, + handler: async (req) => { + const alert = await server.services.pkiAlert.updatePkiAlert({ + alertId: req.params.alertId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: alert.projectId, + event: { + type: EventType.UPDATE_PKI_ALERT, + metadata: { + pkiAlertId: alert.id, + pkiCollectionId: alert.pkiCollectionId, + name: alert.name, + alertBefore: alert.alertBeforeDays.toString(), + eventType: PkiAlertEventType.EXPIRATION, + recipientEmails: alert.recipientEmails + } + } + }); + + return alert; + } + }); + + server.route({ + method: "DELETE", + url: "/:alertId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + tags: [ApiDocsTags.PkiAlerting], + description: "Delete PKI alert", + params: z.object({ + alertId: z.string().trim().describe(ALERTS.DELETE.alertId) + }), + response: { + 200: PkiAlertsSchema + } + }, + handler: async (req) => { + const alert = await server.services.pkiAlert.deletePkiAlert({ + alertId: req.params.alertId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: alert.projectId, + event: { + type: EventType.DELETE_PKI_ALERT, + metadata: { + pkiAlertId: alert.id + } + } + }); + + return alert; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index b480a5144..c27399453 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -11,10 +11,15 @@ import { registerAuthRoutes } from "./auth-router"; import { registerProjectBotRouter } from "./bot-router"; import { registerCaRouter } from "./certificate-authority-router"; import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers"; +import { registerGeneralCertificateAuthorityRouter } from "./certificate-authority-routers/general-certificate-authority-router"; import { registerCertificateProfilesRouter } from "./certificate-profiles-router"; -import { registerCertRouter } from "./certificate-router"; +import { registerCertificateRouter } from "./certificate-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router"; +import { DEPRECATED_CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./deprecated-certificate-authority-routers"; +import { registerDeprecatedCertRouter } from "./deprecated-certificate-router"; +import { registerDeprecatedCertificateTemplateRouter } from "./deprecated-certificate-template-router"; import { registerDeprecatedIdentityProjectMembershipRouter } from "./deprecated-identity-project-membership-router"; +import { registerDeprecatedPkiAlertRouter } from "./deprecated-pki-alert-router"; import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router"; import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router"; import { registerDeprecatedProjectRouter } from "./deprecated-project-router"; @@ -150,21 +155,54 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register( async (pkiRouter) => { - await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); await pkiRouter.register( async (caRouter) => { for await (const [caType, router] of Object.entries(CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP)) { await caRouter.register(router, { prefix: `/${caType}` }); } + + await caRouter.register(registerGeneralCertificateAuthorityRouter); + }, + { + prefix: "/ca" + } + ); + await pkiRouter.register(registerCertificateRouter, { prefix: "/certificates" }); + await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" }); + await pkiRouter.register(registerCertificateProfilesRouter, { prefix: "/certificate-profiles" }); + await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" }); + await pkiRouter.register( + async (pkiSyncRouter) => { + await pkiSyncRouter.register(registerPkiSyncRouter); + for await (const [destination, router] of Object.entries(PKI_SYNC_REGISTER_ROUTER_MAP)) { + await pkiSyncRouter.register(router, { prefix: `/${destination}` }); + } + }, + { prefix: "/syncs" } + ); + }, + { prefix: "/cert-manager" } + ); + + // NOTE: THESE /pki/* ENDPOINTS ARE TO BE DEPRECATED IN FAVOR OF /cert-manager/* + // DO NOT EXTEND THEM ANYMORE!!! + await server.register( + async (pkiRouter) => { + await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); + await pkiRouter.register( + async (caRouter) => { + for await (const [caType, router] of Object.entries(DEPRECATED_CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP)) { + await caRouter.register(router, { prefix: `/${caType}` }); + } }, { prefix: "/ca" } ); - await pkiRouter.register(registerCertRouter, { prefix: "/certificates" }); - await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" }); + await pkiRouter.register(registerDeprecatedCertRouter, { prefix: "/certificates" }); + await pkiRouter.register(registerDeprecatedCertificateTemplateRouter, { prefix: "/certificate-templates" }); await pkiRouter.register(registerCertificateProfilesRouter, { prefix: "/certificate-profiles" }); - await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" }); + await pkiRouter.register(registerDeprecatedPkiAlertRouter, { prefix: "/alerts" }); await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" }); await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" }); await pkiRouter.register( diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 95477c341..7a3b84c5a 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -10,7 +10,12 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema"; import { Integrations } from "@app/services/integration-auth/integration-list"; -import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types"; +import { + PostHogEventTypes, + TIntegrationCreatedEvent, + TIntegrationDeletedEvent, + TIntegrationSyncedEvent +} from "@app/services/telemetry/telemetry-types"; import {} from "../sanitizedSchemas"; @@ -288,31 +293,47 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets }); + const deleteIntegrationEventProperty = shake({ + integrationId: integration.id, + integration: integration.integration, + environment: integration.environment.slug, + secretPath: integration.secretPath, + url: integration.url, + app: integration.app, + appId: integration.appId, + targetEnvironment: integration.targetEnvironment, + targetEnvironmentId: integration.targetEnvironmentId, + targetService: integration.targetService, + targetServiceId: integration.targetServiceId, + path: integration.path, + region: integration.region + }) as TIntegrationDeletedEvent["properties"]; + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: integration.projectId, event: { type: EventType.DELETE_INTEGRATION, // eslint-disable-next-line - metadata: shake({ - integrationId: integration.id, - integration: integration.integration, - environment: integration.environment.slug, - secretPath: integration.secretPath, - url: integration.url, - app: integration.app, - appId: integration.appId, - targetEnvironment: integration.targetEnvironment, - targetEnvironmentId: integration.targetEnvironmentId, - targetService: integration.targetService, - targetServiceId: integration.targetServiceId, - path: integration.path, - region: integration.region, + metadata: { + ...deleteIntegrationEventProperty, shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets // eslint-disable-next-line - }) as any + } as any } }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IntegrationDeleted, + organizationId: req.permission.orgId, + distinctId: getTelemetryDistinctId(req), + properties: { + ...deleteIntegrationEventProperty, + projectId: integration.projectId, + ...req.auditLogInfo + } + }); + return { integration }; } }); @@ -351,28 +372,41 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { id: req.params.integrationId }); + const syncIntegrationEventProperty = shake({ + integrationId: integration.id, + integration: integration.integration, + environment: integration.environment.slug, + secretPath: integration.secretPath, + url: integration.url, + app: integration.app, + appId: integration.appId, + targetEnvironment: integration.targetEnvironment, + targetEnvironmentId: integration.targetEnvironmentId, + targetService: integration.targetService, + targetServiceId: integration.targetServiceId, + path: integration.path, + region: integration.region + }) as TIntegrationSyncedEvent["properties"]; + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: integration.projectId, event: { type: EventType.MANUAL_SYNC_INTEGRATION, // eslint-disable-next-line - metadata: shake({ - integrationId: integration.id, - integration: integration.integration, - environment: integration.environment.slug, - secretPath: integration.secretPath, - url: integration.url, - app: integration.app, - appId: integration.appId, - targetEnvironment: integration.targetEnvironment, - targetEnvironmentId: integration.targetEnvironmentId, - targetService: integration.targetService, - targetServiceId: integration.targetServiceId, - path: integration.path, - region: integration.region - // eslint-disable-next-line - }) as any + metadata: syncIntegrationEventProperty as any + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IntegrationSynced, + organizationId: req.permission.orgId, + distinctId: getTelemetryDistinctId(req), + properties: { + ...syncIntegrationEventProperty, + projectId: integration.projectId, + isManualSync: true, + ...req.auditLogInfo } }); diff --git a/backend/src/server/routes/v1/pki-alert-router.ts b/backend/src/server/routes/v1/pki-alert-router.ts index 60a906c3a..a786e6015 100644 --- a/backend/src/server/routes/v1/pki-alert-router.ts +++ b/backend/src/server/routes/v1/pki-alert-router.ts @@ -1,12 +1,18 @@ import { z } from "zod"; -import { PkiAlertsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { ALERTS, ApiDocsTags } from "@app/lib/api-docs"; +import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { PkiAlertEventType } from "@app/services/pki-alert-v2/pki-alert-v2-types"; +import { + CreatePkiAlertV2Schema, + createSecureAlertBeforeValidator, + PkiAlertChannelType, + PkiAlertEventType, + PkiFilterRuleSchema, + UpdatePkiAlertV2Schema +} from "@app/services/pki-alert-v2/pki-alert-v2-types"; export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { server.route({ @@ -17,25 +23,41 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + description: "Create a new PKI alert", tags: [ApiDocsTags.PkiAlerting], - description: "Create PKI alert", - body: z.object({ - projectId: z.string().trim().describe(ALERTS.CREATE.projectId), - pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId), - name: z.string().trim().describe(ALERTS.CREATE.name), - alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays), - emails: z - .array(z.string().trim().email({ message: "Invalid email address" })) - .min(1, { message: "You must specify at least 1 email" }) - .max(5, { message: "You can specify a maximum of 5 emails" }) - .describe(ALERTS.CREATE.emails) + body: CreatePkiAlertV2Schema.extend({ + projectId: z.string().uuid().describe("Project ID") }), response: { - 200: PkiAlertsSchema + 200: z.object({ + alert: z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable(), + eventType: z.nativeEnum(PkiAlertEventType), + alertBefore: z.string(), + filters: z.array(PkiFilterRuleSchema), + enabled: z.boolean(), + projectId: z.string().uuid(), + channels: z.array( + z.object({ + id: z.string().uuid(), + channelType: z.nativeEnum(PkiAlertChannelType), + config: z.record(z.any()), + enabled: z.boolean(), + createdAt: z.date(), + updatedAt: z.date() + }) + ), + createdAt: z.date(), + updatedAt: z.date() + }) + }) } }, handler: async (req) => { - const alert = await server.services.pkiAlert.createPkiAlert({ + const alert = await server.services.pkiAlertV2.createAlert({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -45,21 +67,80 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: alert.projectId, + projectId: req.body.projectId, event: { type: EventType.CREATE_PKI_ALERT, metadata: { pkiAlertId: alert.id, - pkiCollectionId: alert.pkiCollectionId, name: alert.name, - alertBefore: alert.alertBeforeDays.toString(), - eventType: PkiAlertEventType.EXPIRATION, - recipientEmails: alert.recipientEmails + eventType: alert.eventType, + alertBefore: alert.alertBefore } } }); - return alert; + return { alert }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "List PKI alerts for a project", + tags: [ApiDocsTags.PkiAlerting], + querystring: z.object({ + projectId: z.string().uuid(), + search: z.string().optional(), + eventType: z.nativeEnum(PkiAlertEventType).optional(), + enabled: z.coerce.boolean().optional(), + limit: z.coerce.number().min(1).max(100).default(20), + offset: z.coerce.number().min(0).default(0) + }), + response: { + 200: z.object({ + alerts: z.array( + z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable(), + eventType: z.nativeEnum(PkiAlertEventType), + alertBefore: z.string(), + filters: z.array(PkiFilterRuleSchema), + enabled: z.boolean(), + channels: z.array( + z.object({ + id: z.string().uuid(), + channelType: z.nativeEnum(PkiAlertChannelType), + config: z.record(z.any()), + enabled: z.boolean(), + createdAt: z.date(), + updatedAt: z.date() + }) + ), + createdAt: z.date(), + updatedAt: z.date() + }) + ), + total: z.number() + }) + } + }, + handler: async (req) => { + const alerts = await server.services.pkiAlertV2.listAlerts({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + return alerts; } }); @@ -71,17 +152,41 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + description: "Get a PKI alert by ID", tags: [ApiDocsTags.PkiAlerting], - description: "Get PKI alert", params: z.object({ - alertId: z.string().trim().describe(ALERTS.GET.alertId) + alertId: z.string().uuid().describe("Alert ID") }), response: { - 200: PkiAlertsSchema + 200: z.object({ + alert: z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable(), + eventType: z.nativeEnum(PkiAlertEventType), + alertBefore: z.string(), + filters: z.array(PkiFilterRuleSchema), + enabled: z.boolean(), + projectId: z.string().uuid(), + channels: z.array( + z.object({ + id: z.string().uuid(), + channelType: z.nativeEnum(PkiAlertChannelType), + config: z.record(z.any()), + enabled: z.boolean(), + createdAt: z.date(), + updatedAt: z.date() + }) + ), + createdAt: z.date(), + updatedAt: z.date() + }) + }) } }, handler: async (req) => { - const alert = await server.services.pkiAlert.getPkiAlertById({ + const alert = await server.services.pkiAlertV2.getAlertById({ alertId: req.params.alertId, actor: req.permission.type, actorId: req.permission.id, @@ -100,7 +205,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { } }); - return alert; + return { alert }; } }); @@ -108,32 +213,46 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { method: "PATCH", url: "/:alertId", config: { - rateLimit: readLimit + rateLimit: writeLimit }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + description: "Update a PKI alert", tags: [ApiDocsTags.PkiAlerting], - description: "Update PKI alert", params: z.object({ - alertId: z.string().trim().describe(ALERTS.UPDATE.alertId) - }), - body: z.object({ - name: z.string().trim().optional().describe(ALERTS.UPDATE.name), - alertBeforeDays: z.number().optional().describe(ALERTS.UPDATE.alertBeforeDays), - pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId), - emails: z - .array(z.string().trim().email({ message: "Invalid email address" })) - .min(1, { message: "You must specify at least 1 email" }) - .max(5, { message: "You can specify a maximum of 5 emails" }) - .optional() - .describe(ALERTS.UPDATE.emails) + alertId: z.string().uuid().describe("Alert ID") }), + body: UpdatePkiAlertV2Schema, response: { - 200: PkiAlertsSchema + 200: z.object({ + alert: z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable(), + eventType: z.nativeEnum(PkiAlertEventType), + alertBefore: z.string(), + filters: z.array(PkiFilterRuleSchema), + enabled: z.boolean(), + projectId: z.string().uuid(), + channels: z.array( + z.object({ + id: z.string().uuid(), + channelType: z.nativeEnum(PkiAlertChannelType), + config: z.record(z.any()), + enabled: z.boolean(), + createdAt: z.date(), + updatedAt: z.date() + }) + ), + createdAt: z.date(), + updatedAt: z.date() + }) + }) } }, handler: async (req) => { - const alert = await server.services.pkiAlert.updatePkiAlert({ + const alert = await server.services.pkiAlertV2.updateAlert({ alertId: req.params.alertId, actor: req.permission.type, actorId: req.permission.id, @@ -149,16 +268,14 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { type: EventType.UPDATE_PKI_ALERT, metadata: { pkiAlertId: alert.id, - pkiCollectionId: alert.pkiCollectionId, name: alert.name, - alertBefore: alert.alertBeforeDays.toString(), - eventType: PkiAlertEventType.EXPIRATION, - recipientEmails: alert.recipientEmails + eventType: alert.eventType, + alertBefore: alert.alertBefore } } }); - return alert; + return { alert }; } }); @@ -170,17 +287,41 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + hide: false, + description: "Delete a PKI alert", tags: [ApiDocsTags.PkiAlerting], - description: "Delete PKI alert", params: z.object({ - alertId: z.string().trim().describe(ALERTS.DELETE.alertId) + alertId: z.string().uuid().describe("Alert ID") }), response: { - 200: PkiAlertsSchema + 200: z.object({ + alert: z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable(), + eventType: z.nativeEnum(PkiAlertEventType), + alertBefore: z.string(), + filters: z.array(PkiFilterRuleSchema), + enabled: z.boolean(), + projectId: z.string().uuid(), + channels: z.array( + z.object({ + id: z.string().uuid(), + channelType: z.nativeEnum(PkiAlertChannelType), + config: z.record(z.any()), + enabled: z.boolean(), + createdAt: z.date(), + updatedAt: z.date() + }) + ), + createdAt: z.date(), + updatedAt: z.date() + }) + }) } }, handler: async (req) => { - const alert = await server.services.pkiAlert.deletePkiAlert({ + const alert = await server.services.pkiAlertV2.deleteAlert({ alertId: req.params.alertId, actor: req.permission.type, actorId: req.permission.id, @@ -199,7 +340,111 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { } }); - return alert; + return { alert }; + } + }); + + server.route({ + method: "GET", + url: "/:alertId/certificates", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "List certificates that match an alert's filter rules", + tags: [ApiDocsTags.PkiAlerting], + params: z.object({ + alertId: z.string().uuid().describe("Alert ID") + }), + querystring: z.object({ + limit: z.coerce.number().min(1).max(100).default(20), + offset: z.coerce.number().min(0).default(0) + }), + response: { + 200: z.object({ + certificates: z.array( + z.object({ + id: z.string().uuid(), + serialNumber: z.string(), + commonName: z.string(), + san: z.array(z.string()), + profileName: z.string().nullable(), + enrollmentType: z.string().nullable(), + notBefore: z.date(), + notAfter: z.date(), + status: z.string() + }) + ), + total: z.number() + }) + } + }, + handler: async (req) => { + const result = await server.services.pkiAlertV2.listMatchingCertificates({ + alertId: req.params.alertId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + return result; + } + }); + + server.route({ + method: "POST", + url: "/preview/certificates", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "Preview certificates that would match the given filter rules", + tags: [ApiDocsTags.PkiAlerting], + body: z.object({ + projectId: z.string().uuid().describe("Project ID"), + filters: z.array(PkiFilterRuleSchema), + alertBefore: z + .string() + .refine(createSecureAlertBeforeValidator(), "Must be in format like '30d', '1w', '3m', '1y'") + .describe("Alert timing (e.g., '30d', '1w')"), + limit: z.coerce.number().min(1).max(100).default(20), + offset: z.coerce.number().min(0).default(0) + }), + response: { + 200: z.object({ + certificates: z.array( + z.object({ + id: z.string().uuid(), + serialNumber: z.string(), + commonName: z.string(), + san: z.array(z.string()), + profileName: z.string().nullable(), + enrollmentType: z.string().nullable(), + notBefore: z.date(), + notAfter: z.date(), + status: z.string() + }) + ), + total: z.number() + }) + } + }, + handler: async (req) => { + const result = await server.services.pkiAlertV2.listCurrentMatchingCertificates({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + return result; } }); }; diff --git a/backend/src/server/routes/v2/certificate-templates-v2-router.ts b/backend/src/server/routes/v2/deprecated-certificate-templates-v2-router.ts similarity index 100% rename from backend/src/server/routes/v2/certificate-templates-v2-router.ts rename to backend/src/server/routes/v2/deprecated-certificate-templates-v2-router.ts diff --git a/backend/src/server/routes/v2/index.ts b/backend/src/server/routes/v2/index.ts index d3d91a3ba..7a747a697 100644 --- a/backend/src/server/routes/v2/index.ts +++ b/backend/src/server/routes/v2/index.ts @@ -1,5 +1,5 @@ import { registerCaRouter } from "./certificate-authority-router"; -import { registerCertificateTemplatesV2Router } from "./certificate-templates-v2-router"; +import { registerCertificateTemplatesV2Router } from "./deprecated-certificate-templates-v2-router"; import { registerDeprecatedGroupProjectRouter } from "./deprecated-group-project-router"; import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-router"; import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router"; diff --git a/backend/src/server/routes/v3/certificates-router.ts b/backend/src/server/routes/v3/deprecated-certificates-router.ts similarity index 100% rename from backend/src/server/routes/v3/certificates-router.ts rename to backend/src/server/routes/v3/deprecated-certificates-router.ts diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts index 4ee4566c1..c770d0890 100644 --- a/backend/src/server/routes/v3/index.ts +++ b/backend/src/server/routes/v3/index.ts @@ -1,4 +1,4 @@ -import { registerCertificatesRouter } from "./certificates-router"; +import { registerCertificatesRouter } from "./deprecated-certificates-router"; import { registerDeprecatedSecretRouter } from "./deprecated-secret-router"; import { registerExternalMigrationRouter } from "./external-migration-router"; import { registerLoginRouter } from "./login-router"; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 1c184a436..8e0260c01 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -29,6 +29,7 @@ export enum AppConnection { Flyio = "flyio", GitLab = "gitlab", Cloudflare = "cloudflare", + DNSMadeEasy = "dns-made-easy", Zabbix = "zabbix", Railway = "railway", Bitbucket = "bitbucket", diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index aeef2b25b..d8af3773b 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -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, diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 5b8cc3fc1..27d6a27a8 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -32,6 +32,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [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; + 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 => { + // 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 => { + 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(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 => { + 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 = {}; + 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( + 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; +}; diff --git a/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-schema.ts b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-schema.ts new file mode 100644 index 000000000..d968ba768 --- /dev/null +++ b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-schema.ts @@ -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] })); diff --git a/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-service.ts b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-service.ts new file mode 100644 index 000000000..b50c9b73c --- /dev/null +++ b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-service.ts @@ -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; + +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 + }; +}; diff --git a/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-types.ts b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-types.ts new file mode 100644 index 000000000..eff96f6f9 --- /dev/null +++ b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-types.ts @@ -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; + +export type TDNSMadeEasyConnectionInput = z.infer & { + 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; +}; diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts index c4703d49f..09431f4f8 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts @@ -1,4 +1,5 @@ export enum AcmeDnsProvider { Route53 = "route53", - Cloudflare = "cloudflare" + Cloudflare = "cloudflare", + DNSMadeEasy = "dns-made-easy" } diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index 2e1eafbbe..b48b1076f 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -15,6 +15,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"; @@ -45,6 +46,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"; const parseTtlToDays = (ttl: string): number => { @@ -178,6 +180,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, @@ -312,8 +330,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: { @@ -334,14 +355,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: { @@ -362,6 +395,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}`); } @@ -477,7 +519,6 @@ export const AcmeCertificateAuthorityFns = ({ name, projectId, configuration, - enableDirectIssuance, actor, status }: { @@ -485,7 +526,6 @@ export const AcmeCertificateAuthorityFns = ({ name: string; projectId: string; configuration: TCreateAcmeCertificateAuthorityDTO["configuration"]; - enableDirectIssuance: boolean; actor: OrgServiceActor; }) => { if (crypto.isFipsModeEnabled()) { @@ -513,6 +553,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, @@ -525,7 +571,7 @@ export const AcmeCertificateAuthorityFns = ({ const ca = await certificateAuthorityDAL.create( { projectId, - enableDirectIssuance, + enableDirectIssuance: false, name, status }, @@ -573,14 +619,12 @@ export const AcmeCertificateAuthorityFns = ({ id, status, configuration, - enableDirectIssuance, actor, name }: { id: string; status?: CaStatus; configuration: TUpdateAcmeCertificateAuthorityDTO["configuration"]; - enableDirectIssuance?: boolean; actor: OrgServiceActor; name?: string; }) => { @@ -608,6 +652,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) { @@ -641,13 +694,12 @@ export const AcmeCertificateAuthorityFns = ({ ); } - if (name || status || enableDirectIssuance) { + if (name || status) { await certificateAuthorityDAL.updateById( id, { name, - status, - enableDirectIssuance + status }, tx ); diff --git a/backend/src/services/certificate-authority/acme/deprecated-acme-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/acme/deprecated-acme-certificate-authority-schemas.ts new file mode 100644 index 000000000..232475380 --- /dev/null +++ b/backend/src/services/certificate-authority/acme/deprecated-acme-certificate-authority-schemas.ts @@ -0,0 +1,14 @@ +import { CaType } from "../certificate-authority-enums"; +import { + GenericCreateCertificateAuthorityFieldsSchema, + GenericUpdateCertificateAuthorityFieldsSchema +} from "../deprecated-certificate-authority-schemas"; +import { AcmeCertificateAuthorityConfigurationSchema } from "./acme-certificate-authority-schemas"; + +export const CreateAcmeCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(CaType.ACME).extend({ + configuration: AcmeCertificateAuthorityConfigurationSchema +}); + +export const UpdateAcmeCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(CaType.ACME).extend({ + configuration: AcmeCertificateAuthorityConfigurationSchema.optional() +}); diff --git a/backend/src/services/certificate-authority/acme/dns-providers/dns-made-easy.ts b/backend/src/services/certificate-authority/acme/dns-providers/dns-made-easy.ts new file mode 100644 index 000000000..cbfb26a2e --- /dev/null +++ b/backend/src/services/certificate-authority/acme/dns-providers/dns-made-easy.ts @@ -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; + } +}; diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts index 8b2e23bd4..421da28aa 100644 --- a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts @@ -655,7 +655,6 @@ export const AzureAdCsCertificateAuthorityFns = ({ name, projectId, configuration, - enableDirectIssuance, actor, status }: { @@ -663,16 +662,8 @@ export const AzureAdCsCertificateAuthorityFns = ({ name: string; projectId: string; configuration: TCreateAzureAdCsCertificateAuthorityDTO["configuration"]; - enableDirectIssuance: boolean; actor: OrgServiceActor; }) => { - // Azure ADCS does not support direct issuance - enforce this restriction - if (enableDirectIssuance) { - throw new BadRequestError({ - message: "Azure ADCS Certificate Authorities do not support direct issuance" - }); - } - const { azureAdcsConnectionId } = configuration; const appConnection = await appConnectionDAL.findById(azureAdcsConnectionId); @@ -737,24 +728,15 @@ export const AzureAdCsCertificateAuthorityFns = ({ id, status, configuration, - enableDirectIssuance, actor, name }: { id: string; status?: CaStatus; configuration: TUpdateAzureAdCsCertificateAuthorityDTO["configuration"]; - enableDirectIssuance?: boolean; actor: OrgServiceActor; name?: string; }) => { - // Azure ADCS does not support direct issuance - enforce this restriction - if (enableDirectIssuance) { - throw new BadRequestError({ - message: "Azure ADCS Certificate Authorities do not support direct issuance" - }); - } - const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { if (configuration) { const { azureAdcsConnectionId } = configuration; @@ -795,13 +777,12 @@ export const AzureAdCsCertificateAuthorityFns = ({ ); } - if (name || status || enableDirectIssuance !== undefined) { + if (name || status) { await certificateAuthorityDAL.updateById( id, { name, - status, - enableDirectIssuance: false // Always false for Azure ADCS CAs + status }, tx ); diff --git a/backend/src/services/certificate-authority/azure-ad-cs/deprecated-azure-ad-cs-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/azure-ad-cs/deprecated-azure-ad-cs-certificate-authority-schemas.ts new file mode 100644 index 000000000..a695fcec4 --- /dev/null +++ b/backend/src/services/certificate-authority/azure-ad-cs/deprecated-azure-ad-cs-certificate-authority-schemas.ts @@ -0,0 +1,18 @@ +import { CaType } from "../certificate-authority-enums"; +import { + GenericCreateCertificateAuthorityFieldsSchema, + GenericUpdateCertificateAuthorityFieldsSchema +} from "../deprecated-certificate-authority-schemas"; +import { AzureAdCsCertificateAuthorityConfigurationSchema } from "./azure-ad-cs-certificate-authority-schemas"; + +export const CreateAzureAdCsCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema( + CaType.AZURE_AD_CS +).extend({ + configuration: AzureAdCsCertificateAuthorityConfigurationSchema +}); + +export const UpdateAzureAdCsCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema( + CaType.AZURE_AD_CS +).extend({ + configuration: AzureAdCsCertificateAuthorityConfigurationSchema.optional() +}); diff --git a/backend/src/services/certificate-authority/certificate-authority-schemas.ts b/backend/src/services/certificate-authority/certificate-authority-schemas.ts index 5ecc50a4b..b50fb6293 100644 --- a/backend/src/services/certificate-authority/certificate-authority-schemas.ts +++ b/backend/src/services/certificate-authority/certificate-authority-schemas.ts @@ -19,14 +19,10 @@ export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) => z.object({ name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name), projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.CREATE(type).projectId), - enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance), status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status) }); export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) => z.object({ - name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name), - projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.UPDATE(type).projectId), - enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance), status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status) }); diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 132e8b148..be53f08ce 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -38,6 +38,7 @@ import { CaType } from "./certificate-authority-enums"; import { TCertificateAuthority, TCreateCertificateAuthorityDTO, + TDeprecatedUpdateCertificateAuthorityDTO, TUpdateCertificateAuthorityDTO } from "./certificate-authority-types"; import { TExternalCertificateAuthorityDALFactory } from "./external-certificate-authority-dal"; @@ -128,7 +129,7 @@ export const certificateAuthorityServiceFactory = ({ }); const createCertificateAuthority = async ( - { type, projectId, name, enableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO, + { type, projectId, name, configuration, status }: TCreateCertificateAuthorityDTO, actor: OrgServiceActor ) => { const { permission } = await permissionService.getProjectPermission({ @@ -150,7 +151,6 @@ export const certificateAuthorityServiceFactory = ({ ...(configuration as TCreateInternalCertificateAuthorityDTO["configuration"]), isInternal: true, projectId, - enableDirectIssuance, name }); @@ -176,7 +176,6 @@ export const certificateAuthorityServiceFactory = ({ name, projectId, configuration: configuration as TCreateAcmeCertificateAuthorityDTO["configuration"], - enableDirectIssuance, status, actor }); @@ -187,7 +186,6 @@ export const certificateAuthorityServiceFactory = ({ name, projectId, configuration: configuration as TCreateAzureAdCsCertificateAuthorityDTO["configuration"], - enableDirectIssuance, status, actor }); @@ -196,6 +194,63 @@ export const certificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "Invalid certificate authority type" }); }; + const findCertificateAuthorityById = async ({ id, type }: { id: string; type: CaType }, actor: OrgServiceActor) => { + const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id); + + if (!certificateAuthority) + throw new NotFoundError({ + message: `Could not find certificate authority with id "${id}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: certificateAuthority.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateAuthorities + ); + + if (type === CaType.INTERNAL) { + if (!certificateAuthority.internalCa?.id) { + throw new NotFoundError({ + message: `Internal certificate authority with id "${id}" not found` + }); + } + + return { + id: certificateAuthority.id, + type, + enableDirectIssuance: certificateAuthority.enableDirectIssuance, + name: certificateAuthority.name, + projectId: certificateAuthority.projectId, + configuration: certificateAuthority.internalCa, + status: certificateAuthority.status + } as TCertificateAuthority; + } + + if (certificateAuthority.externalCa?.type !== type) { + throw new NotFoundError({ + message: `Could not find external certificate authority with id ${id} and type "${type}"` + }); + } + + if (type === CaType.ACME) { + return castDbEntryToAcmeCertificateAuthority(certificateAuthority); + } + + if (type === CaType.AZURE_AD_CS) { + return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority); + } + + throw new BadRequestError({ message: "Invalid certificate authority type" }); + }; + const findCertificateAuthorityByNameAndProjectId = async ( { caName, type, projectId }: { caName: string; type: CaType; projectId: string }, actor: OrgServiceActor @@ -308,7 +363,145 @@ export const certificateAuthorityServiceFactory = ({ }; const updateCertificateAuthority = async ( - { caName, type, configuration, enableDirectIssuance, status, name, projectId }: TUpdateCertificateAuthorityDTO, + { id, type, configuration, status, name }: TUpdateCertificateAuthorityDTO, + actor: OrgServiceActor + ) => { + const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id); + + if (!certificateAuthority) + throw new NotFoundError({ + message: `Could not find certificate authority with id "${id}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: certificateAuthority.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.CertificateAuthorities + ); + + if (type === CaType.INTERNAL) { + if (!certificateAuthority.internalCa?.id) { + throw new NotFoundError({ + message: `Internal certificate authority with id "${id}" not found` + }); + } + + const updatedCa = await internalCertificateAuthorityService.updateCaById({ + isInternal: true, + caId: certificateAuthority.id, + status, + name + }); + + if (!updatedCa.internalCa) { + throw new BadRequestError({ + message: "Failed to update internal certificate authority" + }); + } + + return { + id: updatedCa.id, + type, + enableDirectIssuance: updatedCa.enableDirectIssuance, + name: updatedCa.name, + projectId: updatedCa.projectId, + configuration: updatedCa.internalCa, + status: updatedCa.status + } as TCertificateAuthority; + } + + if (type === CaType.ACME) { + return acmeFns.updateCertificateAuthority({ + id: certificateAuthority.id, + configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"], + actor, + status, + name + }); + } + + if (type === CaType.AZURE_AD_CS) { + return azureAdCsFns.updateCertificateAuthority({ + id: certificateAuthority.id, + configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"], + actor, + status, + name + }); + } + + throw new BadRequestError({ message: "Invalid certificate authority type" }); + }; + + const deleteCertificateAuthority = async ({ id, type }: { id: string; type: CaType }, actor: OrgServiceActor) => { + const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id); + + if (!certificateAuthority) + throw new NotFoundError({ + message: `Could not find certificate authority with id "${id}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: certificateAuthority.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.CertificateAuthorities + ); + + if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) { + throw new BadRequestError({ + message: "Internal certificate authority cannot be deleted" + }); + } + + if (certificateAuthority.externalCa?.id && certificateAuthority.externalCa.type !== type) { + throw new BadRequestError({ + message: "External certificate authority cannot be deleted" + }); + } + + await certificateAuthorityDAL.deleteById(certificateAuthority.id); + + if (type === CaType.INTERNAL) { + return { + id: certificateAuthority.id, + type, + enableDirectIssuance: certificateAuthority.enableDirectIssuance, + name: certificateAuthority.name, + projectId: certificateAuthority.projectId, + configuration: certificateAuthority.internalCa, + status: certificateAuthority.status + } as TCertificateAuthority; + } + + if (type === CaType.ACME) { + return castDbEntryToAcmeCertificateAuthority(certificateAuthority); + } + + if (type === CaType.AZURE_AD_CS) { + return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority); + } + + throw new BadRequestError({ message: "Invalid certificate authority type" }); + }; + + const deprecatedUpdateCertificateAuthority = async ( + { caName, type, configuration, status, name, projectId }: TDeprecatedUpdateCertificateAuthorityDTO, actor: OrgServiceActor ) => { const certificateAuthority = await certificateAuthorityDAL.findByNameAndProjectIdWithAssociatedCa( @@ -344,7 +537,6 @@ export const certificateAuthorityServiceFactory = ({ const updatedCa = await internalCertificateAuthorityService.updateCaById({ isInternal: true, - enableDirectIssuance, caId: certificateAuthority.id, status, name @@ -371,7 +563,6 @@ export const certificateAuthorityServiceFactory = ({ return acmeFns.updateCertificateAuthority({ id: certificateAuthority.id, configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"], - enableDirectIssuance, actor, status, name @@ -382,7 +573,6 @@ export const certificateAuthorityServiceFactory = ({ return azureAdCsFns.updateCertificateAuthority({ id: certificateAuthority.id, configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"], - enableDirectIssuance, actor, status, name @@ -392,7 +582,7 @@ export const certificateAuthorityServiceFactory = ({ throw new BadRequestError({ message: "Invalid certificate authority type" }); }; - const deleteCertificateAuthority = async ( + const deprecatedDeleteCertificateAuthority = async ( { caName, type, projectId }: { caName: string; type: CaType; projectId: string }, actor: OrgServiceActor ) => { @@ -529,11 +719,14 @@ export const certificateAuthorityServiceFactory = ({ return { createCertificateAuthority, - findCertificateAuthorityByNameAndProjectId, + findCertificateAuthorityById, listCertificateAuthoritiesByProjectId, + findCertificateAuthorityByNameAndProjectId, updateCertificateAuthority, deleteCertificateAuthority, getAzureAdcsTemplates, - getCaById + getCaById, + deprecatedUpdateCertificateAuthority, + deprecatedDeleteCertificateAuthority }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 13b5cec40..029c9a760 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -19,9 +19,14 @@ export type TCertificateAuthorityInput = | TAcmeCertificateAuthorityInput | TCreateAzureAdCsCertificateAuthorityDTO; -export type TCreateCertificateAuthorityDTO = Omit; +export type TCreateCertificateAuthorityDTO = Omit; export type TUpdateCertificateAuthorityDTO = Partial> & { + type: CaType; + id: string; +}; + +export type TDeprecatedUpdateCertificateAuthorityDTO = Partial> & { type: CaType; caName: string; projectId: string; diff --git a/backend/src/services/certificate-authority/deprecated-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/deprecated-certificate-authority-schemas.ts new file mode 100644 index 000000000..5ecc50a4b --- /dev/null +++ b/backend/src/services/certificate-authority/deprecated-certificate-authority-schemas.ts @@ -0,0 +1,32 @@ +import z from "zod"; + +import { CertificateAuthoritiesSchema } from "@app/db/schemas"; +import { CertificateAuthorities } from "@app/lib/api-docs/constants"; +import { slugSchema } from "@app/server/lib/schemas"; + +import { CaStatus, CaType } from "./certificate-authority-enums"; + +export const BaseCertificateAuthoritySchema = CertificateAuthoritiesSchema.pick({ + projectId: true, + enableDirectIssuance: true, + name: true, + id: true +}).extend({ + status: z.nativeEnum(CaStatus) +}); + +export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) => + z.object({ + name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name), + projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.CREATE(type).projectId), + enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance), + status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status) + }); + +export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) => + z.object({ + name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name), + projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.UPDATE(type).projectId), + enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance), + status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status) + }); diff --git a/backend/src/services/certificate-authority/internal/deprecated-internal-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/internal/deprecated-internal-certificate-authority-schemas.ts new file mode 100644 index 000000000..292af17c9 --- /dev/null +++ b/backend/src/services/certificate-authority/internal/deprecated-internal-certificate-authority-schemas.ts @@ -0,0 +1,14 @@ +import { CaType } from "../certificate-authority-enums"; +import { + GenericCreateCertificateAuthorityFieldsSchema, + GenericUpdateCertificateAuthorityFieldsSchema +} from "../deprecated-certificate-authority-schemas"; +import { InternalCertificateAuthorityConfigurationSchema } from "./internal-certificate-authority-schemas"; + +export const CreateInternalCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema( + CaType.INTERNAL +).extend({ + configuration: InternalCertificateAuthorityConfigurationSchema +}); + +export const UpdateInternalCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(CaType.INTERNAL); diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts index 6f730fd72..2c5cb4b97 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts @@ -136,8 +136,8 @@ export const InternalCertificateAuthorityFns = ({ const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const appCfg = getConfig(); - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; - const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`; const extensions: x509.Extension[] = [ new x509.BasicConstraintsExtension(false), @@ -366,8 +366,8 @@ export const InternalCertificateAuthorityFns = ({ const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const appCfg = getConfig(); - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; - const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`; const extensions: x509.Extension[] = [ new x509.BasicConstraintsExtension(false), diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts index 1cf9a8597..e3b6b4b21 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts @@ -11,7 +11,7 @@ import { } from "../certificate-authority-schemas"; import { validateCaDateField } from "../certificate-authority-validators"; -const InternalCertificateAuthorityConfigurationSchema = z +export const InternalCertificateAuthorityConfigurationSchema = z .object({ type: z.nativeEnum(InternalCaType).describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.type), friendlyName: z.string().optional().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.friendlyName), diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index 5e42dc383..35c596cd3 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -34,8 +34,6 @@ import { CertExtendedKeyUsageOIDToName, CertKeyAlgorithm, CertKeyUsage, - CertSignatureAlgorithm, - CertSignatureType, CertStatus, TAltNameMapping } from "../../certificate/certificate-types"; @@ -127,6 +125,22 @@ export const internalCertificateAuthorityServiceFactory = ({ kmsService, permissionService }: TInternalCertificateAuthorityServiceFactoryDep) => { + const $checkSignature = (caKeyAlg: string, requestedKeyType: string, signatureAlgorithm?: string) => { + const isRsaCa = caKeyAlg.startsWith("RSA"); + const isEcdsaCa = caKeyAlg.startsWith("EC") || caKeyAlg.startsWith("ECDSA"); + + // eslint-disable-next-line no-nested-ternary + const caSupports = isRsaCa ? "RSA" : isEcdsaCa ? "ECDSA" : "unknown"; + + const isRequestValid = (requestedKeyType === "RSA" && isRsaCa) || (requestedKeyType === "ECDSA" && isEcdsaCa); + + if (!isRequestValid) { + throw new BadRequestError({ + message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlg}. CA can only sign with ${caSupports}-based signature algorithms.` + }); + } + }; + const createCa = async ({ type, friendlyName, @@ -140,7 +154,6 @@ export const internalCertificateAuthorityServiceFactory = ({ notAfter, maxPathLength, keyAlgorithm, - enableDirectIssuance, name, ...dto }: TCreateCaDTO) => { @@ -192,9 +205,9 @@ export const internalCertificateAuthorityServiceFactory = ({ const ca = await certificateAuthorityDAL.create( { projectId, - enableDirectIssuance, name: name || slugify(`${(friendlyName || dn).slice(0, 16)}-${alphaNumericNanoId(8)}`), - status: type === InternalCaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE + status: type === InternalCaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE, + enableDirectIssuance: false }, tx ); @@ -354,7 +367,7 @@ export const internalCertificateAuthorityServiceFactory = ({ * Update CA with id [caId]. * Note: Used to enable/disable CA */ - const updateCaById = async ({ caId, status, enableDirectIssuance, name, ...dto }: TUpdateCaDTO) => { + const updateCaById = async ({ caId, status, name, ...dto }: TUpdateCaDTO) => { const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); @@ -375,8 +388,8 @@ export const internalCertificateAuthorityServiceFactory = ({ } const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { - if (enableDirectIssuance !== undefined || status !== undefined || name !== undefined) { - await certificateAuthorityDAL.updateById(ca.id, { enableDirectIssuance, status, name }, tx); + if (status !== undefined || name !== undefined) { + await certificateAuthorityDAL.updateById(ca.id, { status, name }, tx); } return certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx); @@ -971,9 +984,9 @@ export const internalCertificateAuthorityServiceFactory = ({ const serialNumber = createSerialNumber(); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`; - const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`; const intermediateCert = await x509.X509CertificateGenerator.create({ serialNumber, subject: csrObj.subject, @@ -1302,26 +1315,7 @@ export const internalCertificateAuthorityServiceFactory = ({ const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]); if (signatureAlgorithm) { - const caKeyAlgorithm = ca.internalCa.keyAlgorithm; - const requestedKeyType = signatureAlgorithm.split("-")[0]; - - const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]); - const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]); - - if ( - (requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) || - (requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa) - ) { - // eslint-disable-next-line no-nested-ternary - const supportedType = isRsaCa - ? CertSignatureAlgorithm.RSA_SHA256.split("-")[0] - : isEcdsaCa - ? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] - : "unknown"; - throw new BadRequestError({ - message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.` - }); - } + $checkSignature(ca.internalCa.keyAlgorithm, signatureAlgorithm.split("-")[0], signatureAlgorithm); } // Determine signing algorithm for certificate signing @@ -1352,8 +1346,8 @@ export const internalCertificateAuthorityServiceFactory = ({ const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const appCfg = getConfig(); - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; - const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`; const extensions: x509.Extension[] = [ new x509.BasicConstraintsExtension(false), @@ -1690,22 +1684,7 @@ export const internalCertificateAuthorityServiceFactory = ({ } if (signatureAlgorithm) { - const caKeyAlgorithm = ca.internalCa.keyAlgorithm; - const requestedKeyType = signatureAlgorithm.split("-")[0]; // Get the first part (RSA, ECDSA) - - const isRsaCa = caKeyAlgorithm.startsWith(CertSignatureType.RSA); - const isEcdsaCa = caKeyAlgorithm.startsWith(CertSignatureType.ECDSA); - - if ( - (requestedKeyType === CertSignatureType.RSA && !isRsaCa) || - (requestedKeyType === CertSignatureType.ECDSA && !isEcdsaCa) - ) { - // eslint-disable-next-line no-nested-ternary - const supportedType = isRsaCa ? CertSignatureType.RSA : isEcdsaCa ? CertSignatureType.ECDSA : "unknown"; - throw new BadRequestError({ - message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.` - }); - } + $checkSignature(ca.internalCa.keyAlgorithm, signatureAlgorithm.split("-")[0], signatureAlgorithm); } const effectiveKeyAlgorithm = (keyAlgorithm || ca.internalCa.keyAlgorithm) as CertKeyAlgorithm; @@ -1728,9 +1707,9 @@ export const internalCertificateAuthorityServiceFactory = ({ }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`; - const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`; const extensions: x509.Extension[] = [ new x509.BasicConstraintsExtension(false), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts index b4b037933..8f4e9c0d6 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts @@ -48,7 +48,6 @@ export type TCreateCaDTO = notAfter?: string; maxPathLength?: number | null; keyAlgorithm: CertKeyAlgorithm; - enableDirectIssuance: boolean; } | ({ isInternal: false; @@ -66,7 +65,6 @@ export type TCreateCaDTO = notAfter?: string; maxPathLength?: number | null; keyAlgorithm: CertKeyAlgorithm; - enableDirectIssuance: boolean; } & Omit); export type TGetCaDTO = { @@ -79,14 +77,12 @@ export type TUpdateCaDTO = caId: string; name?: string; status?: CaStatus; - enableDirectIssuance?: boolean; } | ({ isInternal: false; caId: string; name?: string; status?: CaStatus; - enableDirectIssuance?: boolean; } & Omit); export type TDeleteCaDTO = { diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index d8fb33cd0..21a24a207 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -1279,7 +1279,7 @@ export const certificateV3ServiceFactory = ({ status: CertificateOrderStatus.VALID })), authorizations: [], - finalize: `/api/v3/pki/certificates/orders/${orderId}/completed`, + finalize: `/api/v1/cert-manager/certificates/orders/${orderId}/completed`, certificate: certificateResult.certificate, projectId: certificateResult.projectId, profileName: certificateResult.profileName diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 515ee3ee0..44be47fc7 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -52,7 +52,10 @@ import { } from "./certificate-types"; type TCertificateServiceFactoryDep = { - certificateDAL: Pick; + certificateDAL: Pick< + TCertificateDALFactory, + "findOne" | "deleteById" | "update" | "find" | "transaction" | "create" | "findById" + >; certificateSecretDAL: Pick; certificateBodyDAL: Pick; certificateAuthorityDAL: Pick; @@ -91,8 +94,8 @@ export const certificateServiceFactory = ({ /** * Return details for certificate with serial number [serialNumber] */ - const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => { - const cert = await certificateDAL.findOne({ serialNumber }); + const getCert = async ({ id, serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => { + const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -117,13 +120,14 @@ export const certificateServiceFactory = ({ * Get certificate private key. */ const getCertPrivateKey = async ({ + id, serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertPrivateKeyDTO) => { - const cert = await certificateDAL.findOne({ serialNumber }); + const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -156,8 +160,8 @@ export const certificateServiceFactory = ({ /** * Delete certificate with serial number [serialNumber] */ - const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => { - const cert = await certificateDAL.findOne({ serialNumber }); + const deleteCert = async ({ id, serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => { + const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -193,6 +197,7 @@ export const certificateServiceFactory = ({ * of its issuing CA */ const revokeCert = async ({ + id, serialNumber, revocationReason, actorId, @@ -200,7 +205,7 @@ export const certificateServiceFactory = ({ actor, actorOrgId }: TRevokeCertDTO) => { - const cert = await certificateDAL.findOne({ serialNumber }); + const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber }); if (!cert.caId) { throw new BadRequestError({ @@ -290,8 +295,8 @@ export const certificateServiceFactory = ({ * Return certificate body and certificate chain for certificate with * serial number [serialNumber] */ - const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => { - const cert = await certificateDAL.findOne({ serialNumber }); + const getCertBody = async ({ id, serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => { + const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -584,8 +589,15 @@ export const certificateServiceFactory = ({ * Return certificate body and certificate chain for certificate with * serial number [serialNumber] */ - const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => { - const cert = await certificateDAL.findOne({ serialNumber }); + const getCertBundle = async ({ + id, + serialNumber, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetCertBundleDTO) => { + const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -673,12 +685,13 @@ export const certificateServiceFactory = ({ certificate, certificateChain, privateKey, - serialNumber, + serialNumber: cert.serialNumber, cert }; }; const getCertPkcs12 = async ({ + id, serialNumber, password, alias, @@ -700,7 +713,7 @@ export const certificateServiceFactory = ({ if (!alias || alias.trim() === "") { throw new BadRequestError({ message: "Alias is required for PKCS12 keystore generation" }); } - const cert = await certificateDAL.findOne({ serialNumber }); + const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber }); const { permission } = await permissionService.getProjectPermission({ actor, @@ -718,7 +731,7 @@ export const certificateServiceFactory = ({ // Get certificate bundle (certificate, chain, private key) const { certificate, certificateChain, privateKey } = await getCertBundle({ - serialNumber, + id: cert.id, actor, actorId, actorAuthMethod, diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index 085bb9588..6c9d8b6bc 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -84,20 +84,24 @@ export enum CrlReason { } export type TGetCertDTO = { - serialNumber: string; + id?: string; + serialNumber?: string; } & Omit; export type TDeleteCertDTO = { - serialNumber: string; + id?: string; + serialNumber?: string; } & Omit; export type TRevokeCertDTO = { - serialNumber: string; + id?: string; + serialNumber?: string; revocationReason: CrlReason; } & Omit; export type TGetCertBodyDTO = { - serialNumber: string; + id?: string; + serialNumber?: string; } & Omit; export type TImportCertDTO = { @@ -112,15 +116,18 @@ export type TImportCertDTO = { } & Omit; export type TGetCertPrivateKeyDTO = { - serialNumber: string; + id?: string; + serialNumber?: string; } & Omit; export type TGetCertBundleDTO = { - serialNumber: string; + id?: string; + serialNumber?: string; } & Omit; export type TGetCertPkcs12DTO = { - serialNumber: string; + id?: string; + serialNumber?: string; password: string; alias: string; } & Omit; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 9322e48cb..212cb0894 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -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; }); diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index a253c1e95..a5178f36d 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -99,13 +99,28 @@ export const identityOidcAuthServiceFactory = ({ } const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert }); - const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>( - `${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`, - { - httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined - } - ); + + let discoveryDoc: { jwks_uri: string }; + try { + const response = await axios.get<{ jwks_uri: string }>( + `${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`, + { + httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined + } + ); + discoveryDoc = response.data; + } catch (error) { + throw new UnauthorizedError({ + message: `Access denied: Failed to fetch OIDC discovery document from ${identityOidcAuth.oidcDiscoveryUrl}. ${error instanceof Error ? error.message : String(error)}` + }); + } + const jwksUri = discoveryDoc.jwks_uri; + if (!jwksUri) { + throw new UnauthorizedError({ + message: `Access denied: OIDC discovery document does not contain a jwks_uri. The identity provider may be misconfigured.` + }); + } const decodedToken = crypto.jwt().decode(oidcJwt, { complete: true }); if (!decodedToken) { diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index e4e1d3126..b76e90470 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -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 () => { diff --git a/backend/src/services/membership-identity/membership-identity-dal.ts b/backend/src/services/membership-identity/membership-identity-dal.ts index 4a90e1edd..bcf855c88 100644 --- a/backend/src/services/membership-identity/membership-identity-dal.ts +++ b/backend/src/services/membership-identity/membership-identity-dal.ts @@ -94,6 +94,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"), db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("name").withSchema(TableName.Role).as("roleName"), db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), @@ -180,6 +181,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { label: "roles" as const, mapper: ({ roleSlug, + roleName, membershipRoleId, membershipRole, membershipRoleIsTemporary, @@ -193,6 +195,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { id: membershipRoleId, role: membershipRole, customRoleSlug: roleSlug, + customRoleName: roleName, temporaryRange: membershipRoleTemporaryRange, temporaryMode: membershipRoleTemporaryMode, temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index ebb1ef599..738819fc6 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -524,8 +524,8 @@ export const pkiSubscriberServiceFactory = ({ }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; - const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`; const extensions: x509.Extension[] = [ new x509.BasicConstraintsExtension(false), diff --git a/backend/src/services/pki-templates/pki-templates-service.ts b/backend/src/services/pki-templates/pki-templates-service.ts index e648ab88f..82d856e25 100644 --- a/backend/src/services/pki-templates/pki-templates-service.ts +++ b/backend/src/services/pki-templates/pki-templates-service.ts @@ -466,8 +466,8 @@ export const pkiTemplatesServiceFactory = ({ }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); - const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; - const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; + const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`; + const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`; const extensions: x509.Extension[] = [ new x509.BasicConstraintsExtension(false), diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index 185ab5e94..60310765b 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -1,4 +1,5 @@ import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; +import { TScimServiceFactory } from "@app/ee/services/scim/scim-types"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { TKeyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; import { getConfig } from "@app/lib/config/env"; @@ -29,6 +30,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { orgService: TOrgServiceFactory; userNotificationDAL: Pick; keyValueStoreDAL: Pick; + scimService: Pick; }; export type TDailyResourceCleanUpQueueServiceFactory = ReturnType; @@ -44,6 +46,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ secretVersionV2DAL, identityUniversalAuthClientSecretDAL, serviceTokenService, + scimService, orgService, userNotificationDAL, keyValueStoreDAL @@ -86,6 +89,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ await secretVersionV2DAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions(); await serviceTokenService.notifyExpiringTokens(); + await scimService.notifyExpiringTokens(); await orgService.notifyInvitedUsers(); await auditLogDAL.pruneAuditLog(); await userNotificationDAL.pruneNotifications(); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 01a7f6210..c18a51ea4 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -421,11 +421,12 @@ export const fnSecretBulkDelete = async ({ ); const changes = deletedSecrets - .filter(({ type }) => type === SecretType.Shared) + .filter(({ type, id }) => type === SecretType.Shared && secretVersions[id]) .map(({ id }) => ({ type: CommitType.DELETE, - secretVersionId: secretVersions[id].id + secretVersionId: secretVersions[id]?.id })); + if (changes.length > 0) { if (commitChanges) { commitChanges.push(...changes); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index 559c86843..d42a26eff 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -2254,7 +2254,8 @@ export const secretV2BridgeServiceFactory = ({ ] } }); - if (secretsToDelete.length !== inputSecrets.length) + const secretsToDeleteSet = new Set(secretsToDelete.map((el) => el.key)); + if (secretsToDeleteSet.size !== inputSecrets.length) throw new NotFoundError({ message: `One or more secrets does not exist: ${secretsToDelete.map((el) => el.key).join(", ")}` }); diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index eefa0764f..5246aa8d1 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -64,6 +64,8 @@ import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; +import { TTelemetryServiceFactory } from "../telemetry/telemetry-service"; +import { PostHogEventTypes } from "../telemetry/telemetry-types"; import { TUserDALFactory } from "../user/user-dal"; import { TWebhookDALFactory } from "../webhook/webhook-dal"; import { fnTriggerWebhook } from "../webhook/webhook-fns"; @@ -120,6 +122,7 @@ type TSecretQueueFactoryDep = { reminderService: Pick; eventBusService: TEventBusService; licenseService: Pick; + telemetryService: Pick; }; export type TGetSecrets = { @@ -184,7 +187,8 @@ export const secretQueueFactory = ({ eventBusService, licenseService, membershipUserDAL, - membershipRoleDAL + membershipRoleDAL, + telemetryService }: TSecretQueueFactoryDep) => { const integrationMeter = opentelemetry.metrics.getMeter("Integrations"); const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", { @@ -1029,6 +1033,29 @@ export const secretQueueFactory = ({ isSynced: response?.isSynced ?? true }); + await telemetryService.sendPostHogEvents({ + event: PostHogEventTypes.IntegrationSynced, + distinctId: `project/${projectId}`, + organizationId: project.orgId, + properties: { + integrationId: integration.id, + integration: integration.integration, + environment, + secretPath, + projectId, + url: integration.url ?? undefined, + app: integration.app ?? undefined, + appId: integration.appId ?? undefined, + targetEnvironment: integration.targetEnvironment ?? undefined, + targetEnvironmentId: integration.targetEnvironmentId ?? undefined, + targetService: integration.targetService ?? undefined, + targetServiceId: integration.targetServiceId ?? undefined, + path: integration.path ?? undefined, + region: integration.region ?? undefined, + isManualSync: isManual ?? false + } + }); + // May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check. if (response?.isSynced === false) { integrationsFailedToSync.push({ diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index de759eb9a..8f7b0a1b7 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -214,6 +214,8 @@ export const serviceTokenServiceFactory = ({ break; } + const successfullyNotifiedTokenIds: string[] = []; + // eslint-disable-next-line no-await-in-loop await Promise.all( expiringTokens.map(async (token) => { @@ -228,13 +230,19 @@ export const serviceTokenServiceFactory = ({ url: `${appCfg.SITE_URL}/organizations/${token.orgId}/projects/secret-management/${token.projectId}/access-management?selectedTab=service-tokens` } }); - await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true }); + successfullyNotifiedTokenIds.push(token.id); } catch (error) { logger.error(error, `Failed to send expiration notification for token ${token.id}:`); } }) ); + // Batch update all successfully notified tokens in a single query + if (successfullyNotifiedTokenIds.length > 0) { + // eslint-disable-next-line no-await-in-loop + await serviceTokenDAL.update({ $in: { id: successfullyNotifiedTokenIds } }, { expiryNotificationSent: true }); + } + processedCount += expiringTokens.length; offset += batchSize; } diff --git a/backend/src/services/smtp/emails/DynamicSecretLeaseRevocationFailedTemplate.tsx b/backend/src/services/smtp/emails/DynamicSecretLeaseRevocationFailedTemplate.tsx new file mode 100644 index 000000000..94e2e8f6a --- /dev/null +++ b/backend/src/services/smtp/emails/DynamicSecretLeaseRevocationFailedTemplate.tsx @@ -0,0 +1,68 @@ +import { Heading, Section, Text } from "@react-email/components"; + +import { BaseButton } from "./BaseButton"; +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; + +interface DynamicSecretLeaseRevocationFailedTemplateProps + extends Omit { + siteUrl: string; + dynamicSecretLeaseUrl: string; + dynamicSecretName: string; + projectName: string; + environmentSlug: string; + errorMessage: string; +} + +export const DynamicSecretLeaseRevocationFailedTemplate = ({ + siteUrl, + dynamicSecretLeaseUrl, + dynamicSecretName, + projectName, + environmentSlug, + errorMessage +}: DynamicSecretLeaseRevocationFailedTemplateProps) => { + return ( + + + Dynamic Secret Lease Revocation Failed + +
+ + One or more leases for the dynamic secret {dynamicSecretName} in project{" "} + {projectName} and environment {environmentSlug} have failed to revoke after + multiple attempts. + + + Please review the dynamic secret leases and attempt to revoke them again. + +
+ +
+ + Latest error message + + {errorMessage} +
+ +
+ View Dynamic Secret Leases +
+
+ ); +}; + +export default DynamicSecretLeaseRevocationFailedTemplate; + +DynamicSecretLeaseRevocationFailedTemplate.PreviewProps = { + errorMessage: 'REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "[REDACTED]" - tuple concurrently updated.', + dynamicSecretLeaseUrl: "https://infisical.com/test", + leaseId: "717d5013-7194-49d9-b6ac-6192328c2914", + dynamicSecretName: "postgres-prod-db", + projectName: "Development Team", + environmentSlug: "dev", + siteUrl: "https://infisical.com" +} as DynamicSecretLeaseRevocationFailedTemplateProps; diff --git a/backend/src/services/smtp/emails/ScimTokenExpiryNoticeTemplate.tsx b/backend/src/services/smtp/emails/ScimTokenExpiryNoticeTemplate.tsx new file mode 100644 index 000000000..84b9395b2 --- /dev/null +++ b/backend/src/services/smtp/emails/ScimTokenExpiryNoticeTemplate.tsx @@ -0,0 +1,71 @@ +import { Heading, Section, Text } from "@react-email/components"; +import React from "react"; + +import { BaseButton } from "./BaseButton"; +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; + +interface ScimTokenExpiryNoticeTemplateProps extends Omit { + tokenDescription?: string; + orgName: string; + createdOn: Date; + expiringOn: Date; + url: string; +} + +export const ScimTokenExpiryNoticeTemplate = ({ + tokenDescription, + siteUrl, + orgName, + url, + createdOn, + expiringOn +}: ScimTokenExpiryNoticeTemplateProps) => { + const formatDate = (date: Date) => + date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric" + }); + + const createdOnDisplay = formatDate(createdOn); + const expiringOnDisplay = formatDate(expiringOn); + + return ( + + + SCIM token expiry notice + +
+ + {tokenDescription ? ( + <> + Your SCIM token {tokenDescription} + + ) : ( + "One of your SCIM tokens" + )}{" "} + for {orgName}, created on {createdOnDisplay}, is scheduled to expire on{" "} + {expiringOnDisplay}. + + + If this token is still needed for your external platform sync, please create a new one before it expires to + avoid disruption to your workflow. + +
+
+ Manage SCIM Tokens +
+
+ ); +}; + +export default ScimTokenExpiryNoticeTemplate; + +ScimTokenExpiryNoticeTemplate.PreviewProps = { + orgName: "Example Organization", + siteUrl: "https://infisical.com", + url: "https://infisical.com", + tokenDescription: "Example SCIM Token", + createdOn: new Date("2025-11-27T00:00:00Z"), + expiringOn: new Date("2025-12-27T00:00:00Z") +} as ScimTokenExpiryNoticeTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 692cacbaf..376f6780e 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -19,6 +19,7 @@ export * from "./PasswordSetupTemplate"; export * from "./PkiExpirationAlertTemplate"; export * from "./ProjectAccessRequestTemplate"; export * from "./ProjectInvitationTemplate"; +export * from "./ScimTokenExpiryNoticeTemplate"; export * from "./ScimUserProvisionedTemplate"; export * from "./SecretApprovalRequestBypassedTemplate"; export * from "./SecretApprovalRequestNeedsReviewTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index cef22009a..e1e2e6041 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -28,6 +28,7 @@ import { PkiExpirationAlertTemplate, ProjectAccessRequestTemplate, ProjectInvitationTemplate, + ScimTokenExpiryNoticeTemplate, ScimUserProvisionedTemplate, SecretApprovalRequestBypassedTemplate, SecretApprovalRequestNeedsReviewTemplate, @@ -43,6 +44,7 @@ import { SubOrganizationInvitationTemplate, UnlockAccountTemplate } from "./emails"; +import DynamicSecretLeaseRevocationFailedTemplate from "./emails/DynamicSecretLeaseRevocationFailedTemplate"; export type TSmtpConfig = SMTPTransport.Options; export type TSmtpSendMail = { @@ -74,6 +76,7 @@ export enum SmtpTemplates { SecretLeakIncident = "secretLeakIncident", WorkspaceInvite = "workspaceInvitation", ScimUserProvisioned = "scimUserProvisioned", + ScimTokenExpired = "scimTokenExpired", PkiExpirationAlert = "pkiExpirationAlert", IntegrationSyncFailed = "integrationSyncFailed", SecretSyncFailed = "secretSyncFailed", @@ -89,7 +92,8 @@ export enum SmtpTemplates { SecretScanningV2ScanFailed = "secretScanningV2ScanFailed", SecretScanningV2SecretsDetected = "secretScanningV2SecretsDetected", AccountDeletionConfirmation = "accountDeletionConfirmation", - HealthAlert = "healthAlert" + HealthAlert = "healthAlert", + DynamicSecretLeaseRevocationFailed = "dynamicSecretLeaseRevocationFailed" } export enum SmtpHost { @@ -121,6 +125,7 @@ const EmailTemplateMap: Record> = { [SmtpTemplates.SecretLeakIncident]: SecretLeakIncidentTemplate, [SmtpTemplates.WorkspaceInvite]: ProjectInvitationTemplate, [SmtpTemplates.ScimUserProvisioned]: ScimUserProvisionedTemplate, + [SmtpTemplates.ScimTokenExpired]: ScimTokenExpiryNoticeTemplate, [SmtpTemplates.SecretRequestCompleted]: SecretRequestCompletedTemplate, [SmtpTemplates.UnlockAccount]: UnlockAccountTemplate, [SmtpTemplates.ServiceTokenExpired]: ServiceTokenExpiryNoticeTemplate, @@ -137,7 +142,8 @@ const EmailTemplateMap: Record> = { [SmtpTemplates.SecretScanningV2ScanFailed]: SecretScanningScanFailedTemplate, [SmtpTemplates.SecretScanningV2SecretsDetected]: SecretScanningSecretsDetectedTemplate, [SmtpTemplates.AccountDeletionConfirmation]: AccountDeletionConfirmationTemplate, - [SmtpTemplates.HealthAlert]: HealthAlertTemplate + [SmtpTemplates.HealthAlert]: HealthAlertTemplate, + [SmtpTemplates.DynamicSecretLeaseRevocationFailed]: DynamicSecretLeaseRevocationFailedTemplate }; export const smtpServiceFactory = (cfg: TSmtpConfig) => { diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index de466614a..d2e977605 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -21,6 +21,8 @@ export enum PostHogEventTypes { SecretScannerPush = "cloud secret scan", ProjectCreated = "Project Created", IntegrationCreated = "Integration Created", + IntegrationSynced = "Integration Synced", + IntegrationDeleted = "Integration Deleted", MachineIdentityCreated = "Machine Identity Created", UserOrgInvitation = "User Org Invitation", TelemetryInstanceStats = "Self Hosted Instance Stats", @@ -126,6 +128,47 @@ export type TIntegrationCreatedEvent = { }; }; +export type TIntegrationSyncedEvent = { + event: PostHogEventTypes.IntegrationSynced; + properties: { + projectId: string; + integrationId: string; + integration: string; + environment: string; + secretPath: string; + isManualSync: boolean; + url?: string; + app?: string; + appId?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; + targetService?: string; + targetServiceId?: string; + path?: string; + region?: string; + }; +}; + +export type TIntegrationDeletedEvent = { + event: PostHogEventTypes.IntegrationDeleted; + properties: { + projectId: string; + integrationId: string; + integration: string; + environment: string; + secretPath: string; + url?: string; + app?: string; + appId?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; + targetService?: string; + targetServiceId?: string; + path?: string; + region?: string; + }; +}; + export type TUserOrgInvitedEvent = { event: PostHogEventTypes.UserOrgInvitation; properties: { @@ -249,6 +292,8 @@ export type TPostHogEvent = { distinctId: string; organizationId?: string } & ( | TUserOrgInvitedEvent | TMachineIdentityCreatedEvent | TIntegrationCreatedEvent + | TIntegrationSyncedEvent + | TIntegrationDeletedEvent | TProjectCreateEvent | TTelemetryInstanceStatsEvent | TSecretRequestCreatedEvent diff --git a/company/documentation/getting-started/introduction.mdx b/company/documentation/getting-started/introduction.mdx index 55b483194..74694703e 100644 --- a/company/documentation/getting-started/introduction.mdx +++ b/company/documentation/getting-started/introduction.mdx @@ -95,12 +95,4 @@ Depending on your use case, it might be helpful to look into some of the resourc > Fetch secrets via HTTP request. - - Explore integrations for GitHub, Vercel, AWS, and more. - diff --git a/docker-swarm/.env-example b/docker-swarm/.env-example index a30e3bba6..8a132914b 100644 --- a/docker-swarm/.env-example +++ b/docker-swarm/.env-example @@ -25,22 +25,6 @@ SMTP_FROM_NAME= SMTP_USERNAME= SMTP_PASSWORD= -# Integration -# Optional only if integration is used -CLIENT_ID_HEROKU= -CLIENT_ID_VERCEL= -CLIENT_ID_NETLIFY= -CLIENT_ID_GITHUB= -CLIENT_ID_GITLAB= -CLIENT_ID_BITBUCKET= -CLIENT_SECRET_HEROKU= -CLIENT_SECRET_VERCEL= -CLIENT_SECRET_NETLIFY= -CLIENT_SECRET_GITHUB= -CLIENT_SECRET_GITLAB= -CLIENT_SECRET_BITBUCKET= -CLIENT_SLUG_VERCEL= - # Sentry (optional) for monitoring errors SENTRY_DSN= diff --git a/docs/api-reference/endpoints/certificate-authorities/acme/create.mdx b/docs/api-reference/endpoints/certificate-authorities/acme/create.mdx index 9cc42ed7f..ef98a7fa1 100644 --- a/docs/api-reference/endpoints/certificate-authorities/acme/create.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/acme/create.mdx @@ -1,4 +1,4 @@ --- title: "Create" -openapi: "POST /api/v1/pki/ca/acme" +openapi: "POST /api/v1/cert-manager/ca/acme" --- diff --git a/docs/api-reference/endpoints/certificate-authorities/acme/delete.mdx b/docs/api-reference/endpoints/certificate-authorities/acme/delete.mdx index 9decc3b6e..eac21ef03 100644 --- a/docs/api-reference/endpoints/certificate-authorities/acme/delete.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/acme/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/pki/ca/acme/{caName}" +openapi: "DELETE /api/v1/cert-manager/ca/acme/{id}" --- diff --git a/docs/api-reference/endpoints/certificate-authorities/acme/list.mdx b/docs/api-reference/endpoints/certificate-authorities/acme/list.mdx index 35bd70727..569efb9af 100644 --- a/docs/api-reference/endpoints/certificate-authorities/acme/list.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/acme/list.mdx @@ -1,4 +1,4 @@ --- title: "List" -openapi: "GET /api/v1/pki/ca/acme" +openapi: "GET /api/v1/cert-manager/ca/acme" --- diff --git a/docs/api-reference/endpoints/certificate-authorities/acme/read.mdx b/docs/api-reference/endpoints/certificate-authorities/acme/read.mdx index a80e31f9a..55f022a3c 100644 --- a/docs/api-reference/endpoints/certificate-authorities/acme/read.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/acme/read.mdx @@ -1,4 +1,4 @@ --- title: "Read" -openapi: "GET /api/v1/pki/ca/acme/{caName}" +openapi: "GET /api/v1/cert-manager/ca/acme/{id}" --- diff --git a/docs/api-reference/endpoints/certificate-authorities/acme/update.mdx b/docs/api-reference/endpoints/certificate-authorities/acme/update.mdx index 69f758771..f9be04fda 100644 --- a/docs/api-reference/endpoints/certificate-authorities/acme/update.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/acme/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v1/pki/ca/acme/{caName}" +openapi: "PATCH /api/v1/cert-manager/ca/acme/{id}" --- diff --git a/docs/api-reference/endpoints/certificate-authorities/cert.mdx b/docs/api-reference/endpoints/certificate-authorities/cert.mdx deleted file mode 100644 index 3706e0b11..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/cert.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Retrieve certificate / chain" -openapi: "GET /api/v1/pki/ca/{caId}/certificate" ---- diff --git a/docs/api-reference/endpoints/certificate-authorities/create.mdx b/docs/api-reference/endpoints/certificate-authorities/create.mdx deleted file mode 100644 index 276015228..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/create.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Create (Deprecated)" -openapi: "POST /api/v1/pki/ca" ---- - - - This endpoint is deprecated. Please use the internal CA endpoint [here](/api-reference/endpoints/certificate-authorities/internal/create). - \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-authorities/crl.mdx b/docs/api-reference/endpoints/certificate-authorities/crl.mdx deleted file mode 100644 index 428c3377e..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/crl.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "List CRLs" -openapi: "GET /api/v1/pki/ca/{caId}/crls" ---- diff --git a/docs/api-reference/endpoints/certificate-authorities/csr.mdx b/docs/api-reference/endpoints/certificate-authorities/csr.mdx deleted file mode 100644 index 2477a629e..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/csr.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Get CSR" -openapi: "GET /api/v1/pki/ca/{caId}/csr" ---- diff --git a/docs/api-reference/endpoints/certificate-authorities/delete.mdx b/docs/api-reference/endpoints/certificate-authorities/delete.mdx deleted file mode 100644 index c4ded070d..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/delete.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Delete (Deprecated)" -openapi: "DELETE /api/v1/pki/ca/{caId}" ---- - - - This endpoint is deprecated. Please use the internal CA endpoint [here](/api-reference/endpoints/certificate-authorities/internal/delete). - \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-authorities/import-cert.mdx b/docs/api-reference/endpoints/certificate-authorities/import-cert.mdx deleted file mode 100644 index 7f0e40f95..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/import-cert.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Import certificate" -openapi: "POST /api/v1/pki/ca/{caId}/import-certificate" ---- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/cert.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/cert.mdx new file mode 100644 index 000000000..476746c55 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/internal/cert.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve certificate / chain" +openapi: "GET /api/v1/cert-manager/ca/internal/{caId}/certificate" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/create.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/create.mdx index babc144f2..9f1567c61 100644 --- a/docs/api-reference/endpoints/certificate-authorities/internal/create.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/internal/create.mdx @@ -1,4 +1,4 @@ --- title: "Create" -openapi: "POST /api/v1/pki/ca/internal" +openapi: "POST /api/v1/cert-manager/ca/internal" --- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/crl.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/crl.mdx new file mode 100644 index 000000000..3a9bb4c62 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/internal/crl.mdx @@ -0,0 +1,4 @@ +--- +title: "List CRLs" +openapi: "GET /api/v1/cert-manager/ca/internal/{caId}/crls" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/csr.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/csr.mdx new file mode 100644 index 000000000..4a2e72505 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/internal/csr.mdx @@ -0,0 +1,4 @@ +--- +title: "Get CSR" +openapi: "GET /api/v1/cert-manager/ca/internal/{caId}/csr" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/delete.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/delete.mdx index b1b7f20a7..7e38781ec 100644 --- a/docs/api-reference/endpoints/certificate-authorities/internal/delete.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/internal/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/pki/ca/internal/{caName}" +openapi: "DELETE /api/v1/cert-manager/ca/internal/{id}" --- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/import-cert.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/import-cert.mdx new file mode 100644 index 000000000..ba4aeb2d0 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/internal/import-cert.mdx @@ -0,0 +1,4 @@ +--- +title: "Import certificate" +openapi: "POST /api/v1/cert-manager/ca/internal/{caId}/import-certificate" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/list-ca-certs.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/list-ca-certs.mdx new file mode 100644 index 000000000..b29444b73 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/internal/list-ca-certs.mdx @@ -0,0 +1,4 @@ +--- +title: "List CA certificates" +openapi: "GET /api/v1/cert-manager/ca/internal/{caId}/ca-certificates" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/list.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/list.mdx index 43f2b7108..bfced601b 100644 --- a/docs/api-reference/endpoints/certificate-authorities/internal/list.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/internal/list.mdx @@ -1,4 +1,4 @@ --- title: "List" -openapi: "GET /api/v1/pki/ca/internal" +openapi: "GET /api/v1/cert-manager/ca/internal" --- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/read.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/read.mdx index d269564cf..85f9582df 100644 --- a/docs/api-reference/endpoints/certificate-authorities/internal/read.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/internal/read.mdx @@ -1,4 +1,4 @@ --- title: "Read" -openapi: "GET /api/v1/pki/ca/internal/{caName}" +openapi: "GET /api/v1/cert-manager/ca/internal/{id}" --- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/renew.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/renew.mdx new file mode 100644 index 000000000..d32963d3f --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/internal/renew.mdx @@ -0,0 +1,4 @@ +--- +title: "Renew" +openapi: "POST /api/v1/cert-manager/ca/internal/{caId}/renew" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/sign-intermediate.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/sign-intermediate.mdx new file mode 100644 index 000000000..e6d185f95 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-authorities/internal/sign-intermediate.mdx @@ -0,0 +1,4 @@ +--- +title: "Sign intermediate certificate" +openapi: "POST /api/v1/cert-manager/ca/internal/{caId}/sign-intermediate" +--- diff --git a/docs/api-reference/endpoints/certificate-authorities/internal/update.mdx b/docs/api-reference/endpoints/certificate-authorities/internal/update.mdx index b01899884..770704e4c 100644 --- a/docs/api-reference/endpoints/certificate-authorities/internal/update.mdx +++ b/docs/api-reference/endpoints/certificate-authorities/internal/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v1/pki/ca/internal/{caName}" +openapi: "PATCH /api/v1/cert-manager/ca/internal/{id}" --- diff --git a/docs/api-reference/endpoints/certificate-authorities/list-ca-certs.mdx b/docs/api-reference/endpoints/certificate-authorities/list-ca-certs.mdx deleted file mode 100644 index ce253807c..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/list-ca-certs.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "List CA certificates" -openapi: "GET /api/v1/pki/ca/{caId}/ca-certificates" ---- diff --git a/docs/api-reference/endpoints/certificate-authorities/list.mdx b/docs/api-reference/endpoints/certificate-authorities/list.mdx deleted file mode 100644 index 81dd64af6..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/list.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "List (Deprecated)" -openapi: "GET /api/v2/workspace/{slug}/cas" ---- - - - This endpoint is deprecated. Please use the internal CA endpoint [here](/api-reference/endpoints/certificate-authorities/internal/list). - \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-authorities/read.mdx b/docs/api-reference/endpoints/certificate-authorities/read.mdx deleted file mode 100644 index bca5121bd..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/read.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Retrieve (Deprecated)" -openapi: "GET /api/v1/pki/ca/{caId}" ---- - - - This endpoint is deprecated. Please use the internal CA endpoint [here](/api-reference/endpoints/certificate-authorities/internal/read). - \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-authorities/renew.mdx b/docs/api-reference/endpoints/certificate-authorities/renew.mdx deleted file mode 100644 index 901811f2d..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/renew.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Renew" -openapi: "POST /api/v1/pki/ca/{caId}/renew" ---- diff --git a/docs/api-reference/endpoints/certificate-authorities/sign-intermediate.mdx b/docs/api-reference/endpoints/certificate-authorities/sign-intermediate.mdx deleted file mode 100644 index 310bbea26..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/sign-intermediate.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Sign intermediate certificate" -openapi: "POST /api/v1/pki/ca/{caId}/sign-intermediate" ---- diff --git a/docs/api-reference/endpoints/certificate-authorities/update.mdx b/docs/api-reference/endpoints/certificate-authorities/update.mdx deleted file mode 100644 index 0cd88ebf6..000000000 --- a/docs/api-reference/endpoints/certificate-authorities/update.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Update (Deprecated)" -openapi: "PATCH /api/v1/pki/ca/{caId}" ---- - - - This endpoint is deprecated. Please use the internal CA endpoint [here](/api-reference/endpoints/certificate-authorities/internal/update). - \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-profiles/create.mdx b/docs/api-reference/endpoints/certificate-profiles/create.mdx index e24e42207..c6ed780e7 100644 --- a/docs/api-reference/endpoints/certificate-profiles/create.mdx +++ b/docs/api-reference/endpoints/certificate-profiles/create.mdx @@ -1,4 +1,4 @@ --- title: "Create" -openapi: "POST /api/v1/pki/certificate-profiles" +openapi: "POST /api/v1/cert-manager/certificate-profiles" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-profiles/delete.mdx b/docs/api-reference/endpoints/certificate-profiles/delete.mdx index a1762640a..966fce508 100644 --- a/docs/api-reference/endpoints/certificate-profiles/delete.mdx +++ b/docs/api-reference/endpoints/certificate-profiles/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/pki/certificate-profiles/{id}" +openapi: "DELETE /api/v1/cert-manager/certificate-profiles/{id}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-profiles/get-by-id.mdx b/docs/api-reference/endpoints/certificate-profiles/get-by-id.mdx index 38e0c20f8..c3f73e6ac 100644 --- a/docs/api-reference/endpoints/certificate-profiles/get-by-id.mdx +++ b/docs/api-reference/endpoints/certificate-profiles/get-by-id.mdx @@ -1,4 +1,4 @@ --- title: "Get by ID" -openapi: "GET /api/v1/pki/certificate-profiles/{id}" +openapi: "GET /api/v1/cert-manager/certificate-profiles/{id}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-profiles/get-by-slug.mdx b/docs/api-reference/endpoints/certificate-profiles/get-by-slug.mdx index 9013020d6..4bcf7b73a 100644 --- a/docs/api-reference/endpoints/certificate-profiles/get-by-slug.mdx +++ b/docs/api-reference/endpoints/certificate-profiles/get-by-slug.mdx @@ -1,4 +1,4 @@ --- title: "Get by Slug" -openapi: "GET /api/v1/pki/certificate-profiles/slug/{slug}" +openapi: "GET /api/v1/cert-manager/certificate-profiles/slug/{slug}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-profiles/get-latest-active-bundle.mdx b/docs/api-reference/endpoints/certificate-profiles/get-latest-active-bundle.mdx index aa033418d..c1f3daebd 100644 --- a/docs/api-reference/endpoints/certificate-profiles/get-latest-active-bundle.mdx +++ b/docs/api-reference/endpoints/certificate-profiles/get-latest-active-bundle.mdx @@ -1,4 +1,4 @@ --- title: "Get Latest Active Certificate Bundle" -openapi: "GET /api/v1/pki/certificate-profiles/{id}/certificates/latest-active-bundle" +openapi: "GET /api/v1/cert-manager/certificate-profiles/{id}/certificates/latest-active-bundle" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-profiles/list-certificates.mdx b/docs/api-reference/endpoints/certificate-profiles/list-certificates.mdx index d0a690f76..2fac15d1d 100644 --- a/docs/api-reference/endpoints/certificate-profiles/list-certificates.mdx +++ b/docs/api-reference/endpoints/certificate-profiles/list-certificates.mdx @@ -1,4 +1,4 @@ --- title: "List Certificates" -openapi: "GET /api/v1/pki/certificate-profiles/{id}/certificates" +openapi: "GET /api/v1/cert-manager/certificate-profiles/{id}/certificates" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-profiles/list.mdx b/docs/api-reference/endpoints/certificate-profiles/list.mdx index c0f461512..869b0d805 100644 --- a/docs/api-reference/endpoints/certificate-profiles/list.mdx +++ b/docs/api-reference/endpoints/certificate-profiles/list.mdx @@ -1,4 +1,4 @@ --- title: "List" -openapi: "GET /api/v1/pki/certificate-profiles" +openapi: "GET /api/v1/cert-manager/certificate-profiles" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-profiles/update.mdx b/docs/api-reference/endpoints/certificate-profiles/update.mdx index e483cf030..c62af15e0 100644 --- a/docs/api-reference/endpoints/certificate-profiles/update.mdx +++ b/docs/api-reference/endpoints/certificate-profiles/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v1/pki/certificate-profiles/{id}" +openapi: "PATCH /api/v1/cert-manager/certificate-profiles/{id}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/list.mdx b/docs/api-reference/endpoints/certificate-syncs/list.mdx index 6de2c2d1b..718a07379 100644 --- a/docs/api-reference/endpoints/certificate-syncs/list.mdx +++ b/docs/api-reference/endpoints/certificate-syncs/list.mdx @@ -1,4 +1,4 @@ --- title: "List PKI Syncs" -openapi: "GET /api/v1/pki/syncs" +openapi: "GET /api/v1/cert-manager/syncs" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/options.mdx b/docs/api-reference/endpoints/certificate-syncs/options.mdx index ab2d11e48..148476128 100644 --- a/docs/api-reference/endpoints/certificate-syncs/options.mdx +++ b/docs/api-reference/endpoints/certificate-syncs/options.mdx @@ -1,4 +1,4 @@ --- title: "Options" -openapi: "GET /api/v1/pki/syncs/options" +openapi: "GET /api/v1/cert-manager/syncs/options" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-templates-v2/create.mdx b/docs/api-reference/endpoints/certificate-templates-v2/create.mdx deleted file mode 100644 index 2fb4da177..000000000 --- a/docs/api-reference/endpoints/certificate-templates-v2/create.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Create" -openapi: "POST /api/v2/certificate-templates" ---- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-templates-v2/delete.mdx b/docs/api-reference/endpoints/certificate-templates-v2/delete.mdx deleted file mode 100644 index dc92ca55a..000000000 --- a/docs/api-reference/endpoints/certificate-templates-v2/delete.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Delete" -openapi: "DELETE /api/v2/certificate-templates/{id}" ---- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-templates-v2/get-by-id.mdx b/docs/api-reference/endpoints/certificate-templates-v2/get-by-id.mdx deleted file mode 100644 index c97389a1d..000000000 --- a/docs/api-reference/endpoints/certificate-templates-v2/get-by-id.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Get by ID" -openapi: "GET /api/v2/certificate-templates/{id}" ---- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-templates-v2/list.mdx b/docs/api-reference/endpoints/certificate-templates-v2/list.mdx deleted file mode 100644 index ab752e851..000000000 --- a/docs/api-reference/endpoints/certificate-templates-v2/list.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "List" -openapi: "GET /api/v2/certificate-templates" ---- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-templates-v2/update.mdx b/docs/api-reference/endpoints/certificate-templates-v2/update.mdx deleted file mode 100644 index 7bdeca14e..000000000 --- a/docs/api-reference/endpoints/certificate-templates-v2/update.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Update" -openapi: "PATCH /api/v2/certificate-templates/{id}" ---- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-templates/create.mdx b/docs/api-reference/endpoints/certificate-templates/create.mdx new file mode 100644 index 000000000..af59acd8d --- /dev/null +++ b/docs/api-reference/endpoints/certificate-templates/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/cert-manager/certificate-templates" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-templates/delete.mdx b/docs/api-reference/endpoints/certificate-templates/delete.mdx new file mode 100644 index 000000000..9232cdef8 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-templates/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/cert-manager/certificate-templates/{id}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-templates/get-by-id.mdx b/docs/api-reference/endpoints/certificate-templates/get-by-id.mdx new file mode 100644 index 000000000..8691cadb0 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-templates/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/cert-manager/certificate-templates/{id}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-templates/list.mdx b/docs/api-reference/endpoints/certificate-templates/list.mdx new file mode 100644 index 000000000..5cdedb2f8 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-templates/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/cert-manager/certificate-templates" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-templates/update.mdx b/docs/api-reference/endpoints/certificate-templates/update.mdx new file mode 100644 index 000000000..229bf6d14 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-templates/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/cert-manager/certificate-templates/{id}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificates/bundle.mdx b/docs/api-reference/endpoints/certificates/bundle.mdx index 60d37a2d8..5f5b5a8b8 100644 --- a/docs/api-reference/endpoints/certificates/bundle.mdx +++ b/docs/api-reference/endpoints/certificates/bundle.mdx @@ -1,6 +1,6 @@ --- title: "Get Certificate Bundle" -openapi: "GET /api/v1/pki/certificates/{serialNumber}/bundle" +openapi: "GET /api/v1/cert-manager/certificates/{id}/bundle" --- diff --git a/docs/api-reference/endpoints/certificates/cert-body.mdx b/docs/api-reference/endpoints/certificates/cert-body.mdx index e4c3b0123..6437ef847 100644 --- a/docs/api-reference/endpoints/certificates/cert-body.mdx +++ b/docs/api-reference/endpoints/certificates/cert-body.mdx @@ -1,4 +1,4 @@ --- title: "Get Certificate Body / Chain" -openapi: "GET /api/v1/pki/certificates/{serialNumber}/certificate" +openapi: "GET /api/v1/cert-manager/certificates/{id}/certificate" --- diff --git a/docs/api-reference/endpoints/certificates/delete.mdx b/docs/api-reference/endpoints/certificates/delete.mdx index 27042af42..2b3d0a74d 100644 --- a/docs/api-reference/endpoints/certificates/delete.mdx +++ b/docs/api-reference/endpoints/certificates/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v1/pki/certificates/{serialNumber}" +openapi: "DELETE /api/v1/cert-manager/certificates/{id}" --- diff --git a/docs/api-reference/endpoints/certificates/issue-certificate.mdx b/docs/api-reference/endpoints/certificates/issue-certificate.mdx index 13a464b67..b77b3caac 100644 --- a/docs/api-reference/endpoints/certificates/issue-certificate.mdx +++ b/docs/api-reference/endpoints/certificates/issue-certificate.mdx @@ -1,4 +1,4 @@ --- title: "Issue Certificate" -openapi: "POST /api/v3/pki/certificates/issue-certificate" +openapi: "POST /api/v1/cert-manager/certificates/issue-certificate" --- diff --git a/docs/api-reference/endpoints/certificates/private-key.mdx b/docs/api-reference/endpoints/certificates/private-key.mdx index d0b93e65c..858baf347 100644 --- a/docs/api-reference/endpoints/certificates/private-key.mdx +++ b/docs/api-reference/endpoints/certificates/private-key.mdx @@ -1,4 +1,4 @@ --- title: "Get Certificate Private Key" -openapi: "GET /api/v1/pki/certificates/{serialNumber}/private-key" +openapi: "GET /api/v1/cert-manager/certificates/{id}/private-key" --- diff --git a/docs/api-reference/endpoints/certificates/read.mdx b/docs/api-reference/endpoints/certificates/read.mdx index ce6463dde..d54d05d09 100644 --- a/docs/api-reference/endpoints/certificates/read.mdx +++ b/docs/api-reference/endpoints/certificates/read.mdx @@ -1,4 +1,4 @@ --- title: "Retrieve" -openapi: "GET /api/v1/pki/certificates/{serialNumber}" +openapi: "GET /api/v1/cert-manager/certificates/{id}" --- diff --git a/docs/api-reference/endpoints/certificates/renew.mdx b/docs/api-reference/endpoints/certificates/renew.mdx index b44424369..8f69be6f6 100644 --- a/docs/api-reference/endpoints/certificates/renew.mdx +++ b/docs/api-reference/endpoints/certificates/renew.mdx @@ -1,4 +1,4 @@ --- title: "Renew Certificate" -openapi: "POST /api/v3/pki/certificates/{certificateId}/renew" +openapi: "POST /api/v1/cert-manager/certificates/{id}/renew" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificates/revoke.mdx b/docs/api-reference/endpoints/certificates/revoke.mdx index e4da73a19..e730412df 100644 --- a/docs/api-reference/endpoints/certificates/revoke.mdx +++ b/docs/api-reference/endpoints/certificates/revoke.mdx @@ -1,4 +1,4 @@ --- title: "Revoke" -openapi: "POST /api/v1/pki/certificates/{serialNumber}/revoke" +openapi: "POST /api/v1/cert-manager/certificates/{id}/revoke" --- diff --git a/docs/api-reference/endpoints/certificates/sign-certificate.mdx b/docs/api-reference/endpoints/certificates/sign-certificate.mdx index 7291025fc..402e8ae08 100644 --- a/docs/api-reference/endpoints/certificates/sign-certificate.mdx +++ b/docs/api-reference/endpoints/certificates/sign-certificate.mdx @@ -1,4 +1,4 @@ --- title: "Sign Certificate" -openapi: "POST /api/v3/pki/certificates/sign-certificate" +openapi: "POST /api/v1/cert-manager/certificates/sign-certificate" --- diff --git a/docs/api-reference/endpoints/certificates/update-config.mdx b/docs/api-reference/endpoints/certificates/update-config.mdx index 70520bf68..cbfe76b29 100644 --- a/docs/api-reference/endpoints/certificates/update-config.mdx +++ b/docs/api-reference/endpoints/certificates/update-config.mdx @@ -1,4 +1,4 @@ --- title: "Update Certificate Config" -openapi: "PATCH /api/v3/pki/certificates/{certificateId}/config" +openapi: "PATCH /api/v1/cert-manager/certificates/{id}/config" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/integrations/create-auth.mdx b/docs/api-reference/endpoints/integrations/create-auth.mdx deleted file mode 100644 index 5af7a0f9c..000000000 --- a/docs/api-reference/endpoints/integrations/create-auth.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: "Create Auth" -openapi: "POST /api/v1/integration-auth/access-token" ---- - -## Integration Authentication Parameters - -The integration authentication endpoint is generic and can be used for all native integrations. -For specific integration parameters for a given service, please review the respective documentation below. - - - - - This value must be **aws-secret-manager**. - - - Infisical project id for the integration. - - - The AWS IAM User Access ID. - - - The AWS IAM User Access Secret Key. - - - - Coming Soon - - - Coming Soon - - diff --git a/docs/api-reference/endpoints/integrations/create.mdx b/docs/api-reference/endpoints/integrations/create.mdx deleted file mode 100644 index 0992e91b9..000000000 --- a/docs/api-reference/endpoints/integrations/create.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: "Create" -openapi: "POST /api/v1/integration" ---- - -## Integration Parameters - -The integration creation endpoint is generic and can be used for all native integrations. -For specific integration parameters for a given service, please review the respective documentation below. - - - - - The ID of the integration auth object for authentication with AWS. - Refer [Create Integration Auth](./create-auth) for more info - - - Whether the integration should be active or inactive - - - The secret name used when saving secret in AWS SSM. Used for naming and can be arbitrary. - - - The AWS region of the SSM. Example: `us-east-1` - - - The Infisical environment slug from where secrets will be synced from. Example: `dev` - - - The Infisical folder path from where secrets will be synced from. Example: `/some/path`. The root of the environment is `/`. - - - - Coming Soon - - - Coming Soon - - - diff --git a/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx b/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx deleted file mode 100644 index 5884363fc..000000000 --- a/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Delete Auth By ID" -openapi: "DELETE /api/v1/integration-auth/{integrationAuthId}" ---- diff --git a/docs/api-reference/endpoints/integrations/delete-auth.mdx b/docs/api-reference/endpoints/integrations/delete-auth.mdx deleted file mode 100644 index 93d957903..000000000 --- a/docs/api-reference/endpoints/integrations/delete-auth.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Delete Auth" -openapi: "DELETE /api/v1/integration-auth" ---- diff --git a/docs/api-reference/endpoints/integrations/delete.mdx b/docs/api-reference/endpoints/integrations/delete.mdx deleted file mode 100644 index 51df56de7..000000000 --- a/docs/api-reference/endpoints/integrations/delete.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Delete" -openapi: "DELETE /api/v1/integration/{integrationId}" ---- diff --git a/docs/api-reference/endpoints/integrations/find-auth.mdx b/docs/api-reference/endpoints/integrations/find-auth.mdx deleted file mode 100644 index 439b82935..000000000 --- a/docs/api-reference/endpoints/integrations/find-auth.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Get Auth By ID" -openapi: "GET /api/v1/integration-auth/{integrationAuthId}" ---- diff --git a/docs/api-reference/endpoints/integrations/list-auth.mdx b/docs/api-reference/endpoints/integrations/list-auth.mdx deleted file mode 100644 index 3ca961d98..000000000 --- a/docs/api-reference/endpoints/integrations/list-auth.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "List Auth" -openapi: "GET /api/v1/workspace/{workspaceId}/authorizations" ---- diff --git a/docs/api-reference/endpoints/integrations/list-project-integrations.mdx b/docs/api-reference/endpoints/integrations/list-project-integrations.mdx deleted file mode 100644 index 24ebbf7d8..000000000 --- a/docs/api-reference/endpoints/integrations/list-project-integrations.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "List Project Integrations" -openapi: "GET /api/v1/workspace/{workspaceId}/integrations" ---- diff --git a/docs/api-reference/endpoints/integrations/update.mdx b/docs/api-reference/endpoints/integrations/update.mdx deleted file mode 100644 index 8567c46ae..000000000 --- a/docs/api-reference/endpoints/integrations/update.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Update" -openapi: "PATCH /api/v1/integration/{integrationId}" ---- diff --git a/docs/api-reference/endpoints/pki-alerts/create.mdx b/docs/api-reference/endpoints/pki-alerts/create.mdx index d4be026a7..e339dd425 100644 --- a/docs/api-reference/endpoints/pki-alerts/create.mdx +++ b/docs/api-reference/endpoints/pki-alerts/create.mdx @@ -1,4 +1,4 @@ --- title: "Create" -openapi: "POST /api/v2/pki/alerts" +openapi: "POST /api/v1/cert-manager/alerts" --- diff --git a/docs/api-reference/endpoints/pki-alerts/delete.mdx b/docs/api-reference/endpoints/pki-alerts/delete.mdx index 67429049c..8cf3b8e40 100644 --- a/docs/api-reference/endpoints/pki-alerts/delete.mdx +++ b/docs/api-reference/endpoints/pki-alerts/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete" -openapi: "DELETE /api/v2/pki/alerts/{alertId}" +openapi: "DELETE /api/v1/cert-manager/alerts/{alertId}" --- diff --git a/docs/api-reference/endpoints/pki-alerts/read.mdx b/docs/api-reference/endpoints/pki-alerts/read.mdx index 0e0547288..b408e1709 100644 --- a/docs/api-reference/endpoints/pki-alerts/read.mdx +++ b/docs/api-reference/endpoints/pki-alerts/read.mdx @@ -1,4 +1,4 @@ --- title: "Retrieve" -openapi: "GET /api/v2/pki/alerts/{alertId}" +openapi: "GET /api/v1/cert-manager/alerts/{alertId}" --- diff --git a/docs/api-reference/endpoints/pki-alerts/update.mdx b/docs/api-reference/endpoints/pki-alerts/update.mdx index 45f1f1f1f..8e4dbb574 100644 --- a/docs/api-reference/endpoints/pki-alerts/update.mdx +++ b/docs/api-reference/endpoints/pki-alerts/update.mdx @@ -1,4 +1,4 @@ --- title: "Update" -openapi: "PATCH /api/v2/pki/alerts/{alertId}" +openapi: "PATCH /api/v1/cert-manager/alerts/{alertId}" --- diff --git a/docs/api-reference/endpoints/pki/syncs/add-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/add-certificates.mdx index c7b21996e..eaa0b6ca1 100644 --- a/docs/api-reference/endpoints/pki/syncs/add-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/add-certificates.mdx @@ -1,4 +1,4 @@ --- title: "Add Certificates to Sync" -openapi: "POST /api/v1/pki/syncs/{pkiSyncId}/certificates" +openapi: "POST /api/v1/cert-manager/syncs/{pkiSyncId}/certificates" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/create.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/create.mdx index dcd58cf32..e4e84fd4c 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/create.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/create.mdx @@ -1,4 +1,4 @@ --- title: "Create AWS Certificate Manager PKI Sync" -openapi: "POST /api/v1/pki/syncs/aws-certificate-manager" +openapi: "POST /api/v1/cert-manager/syncs/aws-certificate-manager" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/delete.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/delete.mdx index 73fed2cdb..0b7bcfbb7 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/delete.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete AWS Certificate Manager PKI Sync" -openapi: "DELETE /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}" +openapi: "DELETE /api/v1/cert-manager/syncs/aws-certificate-manager/{pkiSyncId}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id.mdx index 9191bbde3..7b3a5c14b 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/get-by-id.mdx @@ -1,4 +1,4 @@ --- title: "Get AWS Certificate Manager PKI Sync by ID" -openapi: "GET /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}" +openapi: "GET /api/v1/cert-manager/syncs/aws-certificate-manager/{pkiSyncId}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/list.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/list.mdx index 821ddbd61..e91ef9a21 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/list.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/list.mdx @@ -1,4 +1,4 @@ --- title: "List AWS Certificate Manager PKI Syncs" -openapi: "GET /api/v1/pki/syncs/aws-certificate-manager" +openapi: "GET /api/v1/cert-manager/syncs/aws-certificate-manager" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates.mdx index 5ea989f2a..8d2229b68 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/remove-certificates.mdx @@ -1,4 +1,4 @@ --- title: "Remove Certificates from AWS Certificate Manager" -openapi: "POST /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}/remove-certificates" +openapi: "POST /api/v1/cert-manager/syncs/aws-certificate-manager/{pkiSyncId}/remove-certificates" --- diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates.mdx index b97b7a9ab..2a3fbae8d 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/sync-certificates.mdx @@ -1,4 +1,4 @@ --- title: "Sync Certificates to AWS Certificate Manager" -openapi: "POST /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}/sync" +openapi: "POST /api/v1/cert-manager/syncs/aws-certificate-manager/{pkiSyncId}/sync" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/update.mdx b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/update.mdx index 9b7382ce8..22fdd5a5e 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/update.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-certificate-manager/update.mdx @@ -1,4 +1,4 @@ --- title: "Update AWS Certificate Manager PKI Sync" -openapi: "PATCH /api/v1/pki/syncs/aws-certificate-manager/{pkiSyncId}" +openapi: "PATCH /api/v1/cert-manager/syncs/aws-certificate-manager/{pkiSyncId}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/create.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/create.mdx index 802a6e639..84709ff9d 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/create.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/create.mdx @@ -1,4 +1,4 @@ --- title: "Create AWS Secrets Manager PKI Sync" -openapi: "POST /api/v1/pki/syncs/aws-secrets-manager" +openapi: "POST /api/v1/cert-manager/syncs/aws-secrets-manager" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/delete.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/delete.mdx index 9912a9ee1..22751d5b7 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/delete.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete AWS Secrets Manager PKI Sync" -openapi: "DELETE /api/v1/pki/syncs/aws-secrets-manager/{pkiSyncId}" +openapi: "DELETE /api/v1/cert-manager/syncs/aws-secrets-manager/{pkiSyncId}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/get-by-id.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/get-by-id.mdx index 9b678dcf5..b9e06011d 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/get-by-id.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/get-by-id.mdx @@ -1,4 +1,4 @@ --- title: "Get AWS Secrets Manager PKI Sync by ID" -openapi: "GET /api/v1/pki/syncs/aws-secrets-manager/{pkiSyncId}" +openapi: "GET /api/v1/cert-manager/syncs/aws-secrets-manager/{pkiSyncId}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/list.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/list.mdx index f487770bb..5b933d548 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/list.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/list.mdx @@ -1,4 +1,4 @@ --- title: "List AWS Secrets Manager PKI Syncs" -openapi: "GET /api/v1/pki/syncs/aws-secrets-manager" +openapi: "GET /api/v1/cert-manager/syncs/aws-secrets-manager" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/remove-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/remove-certificates.mdx index f049537ab..ed725eadb 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/remove-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/remove-certificates.mdx @@ -1,4 +1,4 @@ --- title: "Remove Certificates from AWS Secrets Manager" -openapi: "POST /api/v1/pki/syncs/aws-secrets-manager/{pkiSyncId}/remove-certificates" +openapi: "POST /api/v1/cert-manager/syncs/aws-secrets-manager/{pkiSyncId}/remove-certificates" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/sync-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/sync-certificates.mdx index acecf1b83..0af0093bb 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/sync-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/sync-certificates.mdx @@ -1,4 +1,4 @@ --- title: "Sync Certificates to AWS Secrets Manager" -openapi: "POST /api/v1/pki/syncs/aws-secrets-manager/{pkiSyncId}/sync-certificates" +openapi: "POST /api/v1/cert-manager/syncs/aws-secrets-manager/{pkiSyncId}/sync" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/update.mdx b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/update.mdx index b123f3986..807935ee9 100644 --- a/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/update.mdx +++ b/docs/api-reference/endpoints/pki/syncs/aws-secrets-manager/update.mdx @@ -1,4 +1,4 @@ --- title: "Update AWS Secrets Manager PKI Sync" -openapi: "PATCH /api/v1/pki/syncs/aws-secrets-manager/{pkiSyncId}" +openapi: "PATCH /api/v1/cert-manager/syncs/aws-secrets-manager/{pkiSyncId}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/create.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/create.mdx index 1a464cd1e..fb0118ec4 100644 --- a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/create.mdx +++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/create.mdx @@ -1,4 +1,4 @@ --- title: "Create Azure Key Vault PKI Sync" -openapi: "POST /api/v1/pki/syncs/azure-key-vault" +openapi: "POST /api/v1/cert-manager/syncs/azure-key-vault" --- diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/delete.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/delete.mdx index a08b2664d..0f6c686c9 100644 --- a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/delete.mdx +++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete Azure Key Vault PKI Sync" -openapi: "DELETE /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}" +openapi: "DELETE /api/v1/cert-manager/syncs/azure-key-vault/{pkiSyncId}" --- diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/get-by-id.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/get-by-id.mdx index 0976a9dd1..7590402d4 100644 --- a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/get-by-id.mdx +++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/get-by-id.mdx @@ -1,4 +1,4 @@ --- title: "Get Azure Key Vault PKI Sync by ID" -openapi: "GET /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}" +openapi: "GET /api/v1/cert-manager/syncs/azure-key-vault/{pkiSyncId}" --- diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/list.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/list.mdx index b21f5bc33..38b7f9f25 100644 --- a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/list.mdx +++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/list.mdx @@ -1,4 +1,4 @@ --- title: "List Azure Key Vault PKI Syncs" -openapi: "GET /api/v1/pki/syncs/azure-key-vault" +openapi: "GET /api/v1/cert-manager/syncs/azure-key-vault" --- diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx index 817f545c0..eeb8f8116 100644 --- a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx @@ -1,4 +1,4 @@ --- title: "Remove Certificates from Azure Key Vault" -openapi: "POST /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}/remove-certificates" +openapi: "POST /api/v1/cert-manager/syncs/azure-key-vault/{pkiSyncId}/remove-certificates" --- diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates.mdx index ca8faced5..7fd8bebf0 100644 --- a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates.mdx @@ -1,4 +1,4 @@ --- title: "Sync Certificates to Azure Key Vault" -openapi: "POST /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}/sync" +openapi: "POST /api/v1/cert-manager/syncs/azure-key-vault/{pkiSyncId}/sync" --- diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/update.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/update.mdx index bc0e903cf..084d6723e 100644 --- a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/update.mdx +++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/update.mdx @@ -1,4 +1,4 @@ --- title: "Update Azure Key Vault PKI Sync" -openapi: "PATCH /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}" +openapi: "PATCH /api/v1/cert-manager/syncs/azure-key-vault/{pkiSyncId}" --- diff --git a/docs/api-reference/endpoints/pki/syncs/chef/create.mdx b/docs/api-reference/endpoints/pki/syncs/chef/create.mdx index 64807de11..caec0c714 100644 --- a/docs/api-reference/endpoints/pki/syncs/chef/create.mdx +++ b/docs/api-reference/endpoints/pki/syncs/chef/create.mdx @@ -1,4 +1,4 @@ --- title: "Create Chef PKI Sync" -openapi: "POST /api/v1/pki/syncs/chef" +openapi: "POST /api/v1/cert-manager/syncs/chef" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/chef/delete.mdx b/docs/api-reference/endpoints/pki/syncs/chef/delete.mdx index b22dbda83..78bf9c688 100644 --- a/docs/api-reference/endpoints/pki/syncs/chef/delete.mdx +++ b/docs/api-reference/endpoints/pki/syncs/chef/delete.mdx @@ -1,4 +1,4 @@ --- title: "Delete Chef PKI Sync" -openapi: "DELETE /api/v1/pki/syncs/chef/{pkiSyncId}" +openapi: "DELETE /api/v1/cert-manager/syncs/chef/{pkiSyncId}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/chef/get-by-id.mdx b/docs/api-reference/endpoints/pki/syncs/chef/get-by-id.mdx index ece07770e..d0e02566e 100644 --- a/docs/api-reference/endpoints/pki/syncs/chef/get-by-id.mdx +++ b/docs/api-reference/endpoints/pki/syncs/chef/get-by-id.mdx @@ -1,4 +1,4 @@ --- title: "Get Chef PKI Sync by ID" -openapi: "GET /api/v1/pki/syncs/chef/{pkiSyncId}" +openapi: "GET /api/v1/cert-manager/syncs/chef/{pkiSyncId}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/chef/list.mdx b/docs/api-reference/endpoints/pki/syncs/chef/list.mdx index 8e00bed46..84f745b2d 100644 --- a/docs/api-reference/endpoints/pki/syncs/chef/list.mdx +++ b/docs/api-reference/endpoints/pki/syncs/chef/list.mdx @@ -1,4 +1,4 @@ --- title: "List Chef PKI Syncs" -openapi: "GET /api/v1/pki/syncs/chef" +openapi: "GET /api/v1/cert-manager/syncs/chef" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/chef/remove-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/chef/remove-certificates.mdx index f4bb6816a..c8fe5d50a 100644 --- a/docs/api-reference/endpoints/pki/syncs/chef/remove-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/chef/remove-certificates.mdx @@ -1,4 +1,4 @@ --- title: "Remove Certificates from Chef" -openapi: "POST /api/v1/pki/syncs/chef/{pkiSyncId}/remove-certificates" +openapi: "POST /api/v1/cert-manager/syncs/chef/{pkiSyncId}/remove-certificates" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/chef/sync-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/chef/sync-certificates.mdx index 109248d8a..458f58cfa 100644 --- a/docs/api-reference/endpoints/pki/syncs/chef/sync-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/chef/sync-certificates.mdx @@ -1,4 +1,4 @@ --- title: "Sync Certificates to Chef" -openapi: "POST /api/v1/pki/syncs/chef/{pkiSyncId}/sync" +openapi: "POST /api/v1/cert-manager/syncs/chef/{pkiSyncId}/sync" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/chef/update.mdx b/docs/api-reference/endpoints/pki/syncs/chef/update.mdx index 2d08b40e9..ad78c8c66 100644 --- a/docs/api-reference/endpoints/pki/syncs/chef/update.mdx +++ b/docs/api-reference/endpoints/pki/syncs/chef/update.mdx @@ -1,4 +1,4 @@ --- title: "Update Chef PKI Sync" -openapi: "PATCH /api/v1/pki/syncs/chef/{pkiSyncId}" +openapi: "PATCH /api/v1/cert-manager/syncs/chef/{pkiSyncId}" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/get-by-id.mdx b/docs/api-reference/endpoints/pki/syncs/get-by-id.mdx index 6ec710ec8..73131deba 100644 --- a/docs/api-reference/endpoints/pki/syncs/get-by-id.mdx +++ b/docs/api-reference/endpoints/pki/syncs/get-by-id.mdx @@ -1,4 +1,4 @@ --- title: "Get PKI Sync by ID" -openapi: "GET /api/v1/pki/syncs/{pkiSyncId}" +openapi: "GET /api/v1/cert-manager/syncs/{pkiSyncId}" --- diff --git a/docs/api-reference/endpoints/pki/syncs/list-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/list-certificates.mdx index eaece0a2d..994803c0b 100644 --- a/docs/api-reference/endpoints/pki/syncs/list-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/list-certificates.mdx @@ -1,4 +1,4 @@ --- title: "List Sync Certificates" -openapi: "GET /api/v1/pki/syncs/{pkiSyncId}/certificates" +openapi: "GET /api/v1/cert-manager/syncs/{pkiSyncId}/certificates" --- \ No newline at end of file diff --git a/docs/api-reference/endpoints/pki/syncs/list.mdx b/docs/api-reference/endpoints/pki/syncs/list.mdx index 4b1f1972a..6ac0e4841 100644 --- a/docs/api-reference/endpoints/pki/syncs/list.mdx +++ b/docs/api-reference/endpoints/pki/syncs/list.mdx @@ -1,4 +1,4 @@ --- title: "List PKI Syncs" -openapi: "GET /api/v1/pki/syncs" +openapi: "GET /api/v1/cert-manager/syncs" --- diff --git a/docs/api-reference/endpoints/pki/syncs/options.mdx b/docs/api-reference/endpoints/pki/syncs/options.mdx index a328b0832..b615aa4b6 100644 --- a/docs/api-reference/endpoints/pki/syncs/options.mdx +++ b/docs/api-reference/endpoints/pki/syncs/options.mdx @@ -1,4 +1,4 @@ --- title: "List PKI Sync Options" -openapi: "GET /api/v1/pki/syncs/options" +openapi: "GET /api/v1/cert-manager/syncs/options" --- diff --git a/docs/api-reference/endpoints/pki/syncs/remove-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/remove-certificates.mdx index 99c8bfe28..ed5dbf70a 100644 --- a/docs/api-reference/endpoints/pki/syncs/remove-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/remove-certificates.mdx @@ -1,4 +1,4 @@ --- title: "Remove Certificates from Sync" -openapi: "DELETE /api/v1/pki/syncs/{pkiSyncId}/certificates" +openapi: "DELETE /api/v1/cert-manager/syncs/{pkiSyncId}/certificates" --- \ No newline at end of file diff --git a/docs/api-reference/overview/examples/integration.mdx b/docs/api-reference/overview/examples/integration.mdx deleted file mode 100644 index 71f5b6de4..000000000 --- a/docs/api-reference/overview/examples/integration.mdx +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: "Configure native integrations via API" -description: "How to use Infisical API to sync secrets to external secret managers" ---- - -The Infisical API allows you to create programmatic integrations that connect with third-party secret managers to synchronize secrets from Infisical. - -This guide will primarily demonstrate the process using AWS Secret Store Manager (AWS SSM), but the steps are generally applicable to other secret management integrations. - - - For details on setting up AWS SSM synchronization and understanding its prerequisites, refer to the [AWS SSM integration setup documentation](../../../integrations/cloud/aws-secret-manager). - - - - - Authentication is required for all integrations. Use the [Integration Auth API](../../endpoints/integrations/create-auth) with the following parameters to authenticate. - - - Set this parameter to **aws-secret-manager**. - - - The Infisical project ID for the integration. - - - The AWS IAM User Access ID. - - - The AWS IAM User Access Secret Key. - - - ```bash Request - curl --request POST \ - --url https://app.infisical.com/api/v1/integration-auth/access-token \ - --header 'Authorization: ' \ - --header 'Content-Type: application/json' \ - --data '{ - "workspaceId": "", - "integration": "aws-secret-manager", - "accessId": "", - "accessToken": "" - }' - ``` - - - - Once authentication between AWS SSM and Infisical is established, you can configure the synchronization behavior. - This involves specifying the source (environment and secret path in Infisical) and the destination in SSM to which the secrets will be synchronized. - - Use the [integration API](../../endpoints/integrations/create) with the following parameters to configure the sync source and destination. - - - The ID of the integration authentication object used with AWS, obtained from the previous API response. - - - Indicates whether the integration should be active or inactive. - - - The secret name for saving in AWS SSM, which can be arbitrarily chosen. - - - The AWS region where the SSM is located, e.g., `us-east-1`. - - - The Infisical environment slug from which secrets will be synchronized, e.g., `dev`. - - - The Infisical folder path from which secrets will be synchronized, e.g., `/some/path`. The root path is `/`. - - - ```bash Request - curl --request POST \ - --url https://app.infisical.com/api/v1/integration \ - --header 'Authorization: ' \ - --header 'Content-Type: application/json' \ - --data '{ - "integrationAuthId": "", - "sourceEnvironment": "", - "secretPath": "", - "app": "", - "region": "" - }' - ``` - - - - - -Congratulations! You have successfully set up an integration to synchronize secrets from Infisical with AWS SSM. -For more information, [view the integration API reference](../../endpoints/integrations). - \ No newline at end of file diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index c58c13713..a670c03aa 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -10,6 +10,7 @@ infisical login ### Description The CLI uses authentication to verify your identity. You can authenticate using: + - **Browser Login** (default): Opens a browser for authentication - **Direct Login**: Provide email and password via flags or environment variables for non-interactive workflows - **Interactive CLI Login**: Use the `--interactive` flag to enter credentials via CLI prompts @@ -24,9 +25,9 @@ If you have added multiple users, you can switch between the users by using the **JWT Token Output:** - For **user authentication** with the `--plain --silent` flags: outputs only the JWT access token (useful for scripting) - For **machine identity authentication**: an access token is always printed to the console - + Use the `--plain` flag to print only the token in plain text and the `--silent` flag to disable update alerts. - + Both flags are ideal for capturing the token in environment variables or CI/CD pipelines. @@ -500,6 +501,30 @@ The login command supports a number of flags that you can use for different auth The `jwt` flag can be substituted with the `INFISICAL_JWT` environment variable. + + + ```bash + infisical login --domain= + ``` + + #### Description + Specifies the Infisical API URL for non-US Cloud instances. This flag is required when connecting to any instance other than US Cloud (e.g. EU Cloud or self-hosted). + + ```bash + # Example for EU Cloud + infisical login --domain="https://eu.infisical.com" + + # Example for localhost + infisical login --domain="http://localhost:8080" + + # Example for self-hosted + infisical login --domain="https://your-self-hosted-infisical.com" + ``` + + + **Critical:** If you use `--domain` during login, you must also include it on **all subsequent CLI commands** (e.g., `infisical secrets`, `infisical export`, etc.). Alternatively, set the `INFISICAL_API_URL` environment variable to avoid having to use `--domain` on every command. Refer to the [Domain Configuration](/cli/usage#domain-configuration) section for more details. + + @@ -529,8 +554,11 @@ The following examples demonstrate different ways to authenticate as a user with # Basic direct login (defaults to US Cloud) infisical login --email user@example.com --password "your-password" --organization-id "your-organization-id" - # EU Cloud (Custom domain) - infisical login --email user@example.com --password "your-password" --organization-id "your-organization-id" --domain https://eu.infisical.com + # Basic direct login (EU Cloud) + infisical login --domain https://eu.infisical.com --email user@example.com --password "your-password" --organization-id "your-organization-id" + + # Basic direct login (Self-hosted Instance) + infisical login --domain https://your-self-hosted-infisical.com --email user@example.com --password "your-password" --organization-id "your-organization-id" # Output only JWT token for scripting export INFISICAL_TOKEN=$(infisical login --email user@example.com --password "your-password" --organization-id "your-organization-id" --plain --silent) @@ -550,6 +578,11 @@ The following examples demonstrate different ways to authenticate as a user with # Or with plain output for token capture export INFISICAL_TOKEN=$(infisical login --plain --silent) ``` + + + **For non-US Cloud instances:** If you're using EU Cloud or a self-hosted instance, you must set `INFISICAL_API_URL` before login or use `--domain` on all commands. Refer to the [Domain Configuration](/cli/usage#domain-configuration) section for more details. + + @@ -571,7 +604,7 @@ The following examples demonstrate different ways to authenticate as a user with -If you have SSO enabled, we recommend using the default browser login. + If you have SSO enabled, we recommend using the default browser login. ### Machine Identity Authentication Quick Start @@ -584,6 +617,10 @@ In this example we'll be using the `universal-auth` method to login to obtain an export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id= --client-secret= --silent --plain) # silent and plain is important to ensure only the token itself is printed, so we can easily set it as an environment variable. ``` + + **For non-US Cloud instances:** If you're using EU Cloud or a self-hosted instance, you must set `INFISICAL_API_URL` before login or use `--domain` on all commands. Refer to the [Domain Configuration](/cli/usage#domain-configuration) section for more details. + + Now that we've set the `INFISICAL_TOKEN` environment variable, we can use the CLI to interact with Infisical. The CLI will automatically check for the presence of the `INFISICAL_TOKEN` environment variable and use it for authentication. diff --git a/docs/cli/usage.mdx b/docs/cli/usage.mdx index bedfda22c..04a7cb025 100644 --- a/docs/cli/usage.mdx +++ b/docs/cli/usage.mdx @@ -127,10 +127,66 @@ The CLI is designed for a variety of secret management applications ranging from Starting with CLI version v0.4.0, you can now choose to log in via Infisical Cloud (US/EU) or your own self-hosted instance by simply running `infisical login` and following the on-screen instructions — no need to manually set the `INFISICAL_API_URL` environment variable. -For versions prior to v0.4.0, the CLI defaults to the US Cloud. To connect to the EU Cloud or a self-hosted instance, set the `INFISICAL_API_URL` environment variable to `https://eu.infisical.com` or your custom URL. +For versions prior to v0.4.0, the CLI defaults to US Cloud. To connect to EU Cloud or a self-hosted instance, set the `INFISICAL_API_URL` environment variable to `https://eu.infisical.com` or your custom URL. + + ## Domain Configuration + +**Important:** If you're not using interactive login, you must configure the domain for **all CLI commands**. + +The CLI defaults to US Cloud (https://app.infisical.com). To connect to **EU Cloud (https://eu.infisical.com)** or a **self-hosted instance**, you must configure the domain in one of the following ways: + +- Use the `INFISICAL_API_URL` environment variable +- Use the `--domain` flag on every command + + + + The easiest way to ensure all CLI commands use the correct domain is to set + the `INFISICAL_API_URL` environment variable. This applies the domain + setting globally to all commands: + + ```bash + # Linux/MacOS + export INFISICAL_API_URL="https://your-domain.infisical.com" + + # Windows PowerShell + setx INFISICAL_API_URL "https://your-domain.infisical.com" + ``` + + Once set, all subsequent CLI commands will automatically use this domain: + + ```bash + # Login with the domain + infisical login --method=universal-auth --client-id= --client-secret= --silent --plain + + # All other commands will also use the same domain automatically + infisical secrets --projectId --env dev + ``` + + + + The `--domain` flag can be used to set the domain for a single command. This + applies the domain setting to the command only: + + ```bash + # Login with domain + infisical login --domain="https://your-domain.infisical.com" --method=universal-auth --client-id= --client-secret= --silent --plain + + # All subsequent commands must also include --domain + infisical secrets --domain="https://your-domain.infisical.com" --projectId= --env=dev + ``` + + + If you use `--domain` during login but forget to include it on subsequent commands, you may encounter authentication errors. + + + + + + + ## Custom Request Headers @@ -186,51 +242,65 @@ For security and privacy concerns, we recommend you to configure your terminal t ## FAQ - - Yes. The CLI is set to connect to Infisical Cloud by default, but if you're running your own instance of Infisical, you can direct the CLI to it using one of the methods provided below. + + Yes. The CLI is set to connect to Infisical US Cloud by default, but if you're using EU Cloud or a self-hosted instance you can configure the domain for **all CLI commands**. - #### Method 1: Use the updated CLI + #### Method 1: Use the updated CLI (v0.4.0+) - Beginning with CLI version V0.4.0, it is now possible to choose between logging in through the Infisical cloud or your own self-hosted instance. Simply execute the `infisical login` command and follow the on-screen instructions. + Beginning with CLI version V0.4.0, you can choose between logging in through Infisical US Cloud, EU Cloud, or your own self-hosted instance. Simply execute the `infisical login` command and follow the on-screen instructions. - #### Method 2: Export environment variable + #### Method 2: Export environment variable You can point the CLI to the self-hosted Infisical instance by exporting the environment variable `INFISICAL_API_URL` in your terminal. ```bash - # set backend host - export INFISICAL_API_URL="https://your-self-hosted-infisical.com/api" + # Set the API URL + export INFISICAL_API_URL="https://your-self-hosted-infisical.com" - # remove backend host + # For EU Cloud + export INFISICAL_API_URL="https://eu.infisical.com" + + # Remove the setting unset INFISICAL_API_URL ``` ```bash - # set backend host - setx INFISICAL_API_URL "https://your-self-hosted-infisical.com/api" + # Set the API URL + setx INFISICAL_API_URL "https://your-self-hosted-infisical.com" - # remove backend host + # For EU Cloud + setx INFISICAL_API_URL "https://eu.infisical.com" + + # Remove the setting setx INFISICAL_API_URL "" - # NOTE: Once set or removed, please restart powershell for the change to take effect + # NOTE: Once set, please restart powershell for the change to take effect ``` -#### Method 3: Set manually on every command + #### Method 3: Set manually on every command -Another option to point the CLI to your self-hosted Infisical instance is to set it via a flag on every command you run. + If you prefer not to use an environment variable, you must include the `--domain` flag on **every CLI command** you run: -```bash -# Example -infisical --domain="https://your-self-hosted-infisical.com/api" -``` + ```bash + # Login with domain + infisical login --domain="https://your-domain.infisical.com" --method=oidc-auth --jwt $JWT + + # All subsequent commands must also include --domain + infisical secrets --domain="https://your-self-hosted-infisical.com" --projectId --env dev + infisical export --domain="https://your-self-hosted-infisical.com" --format=dotenv-export + ``` + + + **Best Practice:** Use `INFISICAL_API_URL` environment variable (Method 2) to avoid having to remember the `--domain` flag on every command. This is especially important in CI/CD pipelines and automation scripts. + diff --git a/docs/docs.json b/docs/docs.json index 57f44596f..51c02d2bf 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -118,6 +118,7 @@ "integrations/app-connections/cloudflare", "integrations/app-connections/databricks", "integrations/app-connections/digital-ocean", + "integrations/app-connections/dns-made-easy", "integrations/app-connections/flyio", "integrations/app-connections/gcp", "integrations/app-connections/github", @@ -490,6 +491,10 @@ "pages": [ "integrations/platforms/ansible", "integrations/platforms/apache-airflow", + { + "group": "AWS", + "pages": ["integrations/platforms/aws/lambda"] + }, { "group": "Kubernetes Operator", "pages": [ @@ -568,71 +573,14 @@ } ] }, - { - "group": "Native Integrations", - "pages": [ - { - "group": "AWS", - "pages": [ - "integrations/cloud/aws-parameter-store", - "integrations/cloud/aws-secret-manager", - "integrations/cloud/aws-amplify" - ] - }, - "integrations/cloud/vercel", - "integrations/cloud/azure-key-vault", - "integrations/cloud/azure-app-configuration", - "integrations/cloud/azure-devops", - "integrations/cloud/gcp-secret-manager", - { - "group": "Cloudflare", - "pages": [ - "integrations/cloud/cloudflare-pages", - "integrations/cloud/cloudflare-workers" - ] - }, - "integrations/cloud/terraform-cloud", - "integrations/cloud/databricks", - { - "group": "View more", - "pages": [ - "integrations/cloud/digital-ocean-app-platform", - "integrations/cloud/heroku", - "integrations/cloud/netlify", - "integrations/cloud/flyio", - "integrations/cloud/railway", - "integrations/cloud/render", - "integrations/cloud/laravel-forge", - "integrations/cloud/supabase", - "integrations/cloud/northflank", - "integrations/cloud/hasura-cloud", - "integrations/cloud/qovery", - "integrations/cloud/hashicorp-vault", - "integrations/cloud/cloud-66", - "integrations/cloud/windmill" - ] - } - ] - }, { "group": "CI/CD Integrations", "pages": [ - "integrations/cicd/jenkins", + "integrations/cicd/aws-amplify", + "integrations/cicd/bitbucket", "integrations/cicd/githubactions", "integrations/cicd/gitlab", - "integrations/cicd/bitbucket", - "integrations/cloud/teamcity", - { - "group": "View more", - "pages": [ - "integrations/cicd/circleci", - "integrations/cicd/travisci", - "integrations/cicd/rundeck", - "integrations/cicd/codefresh", - "integrations/cloud/checkly", - "integrations/cicd/octopus-deploy" - ] - } + "integrations/cicd/jenkins" ] }, { @@ -752,7 +700,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", @@ -884,11 +832,7 @@ "group": "Overview", "pages": [ "api-reference/overview/introduction", - "api-reference/overview/authentication", - { - "group": "Examples", - "pages": ["api-reference/overview/examples/integration"] - } + "api-reference/overview/authentication" ] }, { @@ -2490,20 +2434,6 @@ ] } ] - }, - { - "group": "Integrations", - "pages": [ - "api-reference/endpoints/integrations/create-auth", - "api-reference/endpoints/integrations/list-auth", - "api-reference/endpoints/integrations/find-auth", - "api-reference/endpoints/integrations/delete-auth", - "api-reference/endpoints/integrations/delete-auth-by-id", - "api-reference/endpoints/integrations/create", - "api-reference/endpoints/integrations/update", - "api-reference/endpoints/integrations/delete", - "api-reference/endpoints/integrations/list-project-integrations" - ] } ] }, @@ -2530,21 +2460,16 @@ "api-reference/endpoints/certificate-authorities/internal/create", "api-reference/endpoints/certificate-authorities/internal/read", "api-reference/endpoints/certificate-authorities/internal/update", - "api-reference/endpoints/certificate-authorities/internal/delete" + "api-reference/endpoints/certificate-authorities/internal/delete", + "api-reference/endpoints/certificate-authorities/internal/renew", + "api-reference/endpoints/certificate-authorities/internal/list-ca-certs", + "api-reference/endpoints/certificate-authorities/internal/csr", + "api-reference/endpoints/certificate-authorities/internal/cert", + "api-reference/endpoints/certificate-authorities/internal/sign-intermediate", + "api-reference/endpoints/certificate-authorities/internal/import-cert", + "api-reference/endpoints/certificate-authorities/internal/crl" ] - }, - "api-reference/endpoints/certificate-authorities/list", - "api-reference/endpoints/certificate-authorities/create", - "api-reference/endpoints/certificate-authorities/read", - "api-reference/endpoints/certificate-authorities/update", - "api-reference/endpoints/certificate-authorities/delete", - "api-reference/endpoints/certificate-authorities/renew", - "api-reference/endpoints/certificate-authorities/list-ca-certs", - "api-reference/endpoints/certificate-authorities/csr", - "api-reference/endpoints/certificate-authorities/cert", - "api-reference/endpoints/certificate-authorities/sign-intermediate", - "api-reference/endpoints/certificate-authorities/import-cert", - "api-reference/endpoints/certificate-authorities/crl" + } ] }, { @@ -2566,23 +2491,11 @@ { "group": "Certificate Templates", "pages": [ - "api-reference/endpoints/certificate-templates-v2/list", - "api-reference/endpoints/certificate-templates-v2/create", - "api-reference/endpoints/certificate-templates-v2/update", - "api-reference/endpoints/certificate-templates-v2/get-by-id", - "api-reference/endpoints/certificate-templates-v2/delete" - ] - }, - { - "group": "Certificate Collections", - "pages": [ - "api-reference/endpoints/pki-collections/create", - "api-reference/endpoints/pki-collections/read", - "api-reference/endpoints/pki-collections/update", - "api-reference/endpoints/pki-collections/delete", - "api-reference/endpoints/pki-collections/add-item", - "api-reference/endpoints/pki-collections/list-items", - "api-reference/endpoints/pki-collections/delete-item" + "api-reference/endpoints/certificate-templates/list", + "api-reference/endpoints/certificate-templates/create", + "api-reference/endpoints/certificate-templates/update", + "api-reference/endpoints/certificate-templates/get-by-id", + "api-reference/endpoints/certificate-templates/delete" ] }, { @@ -2598,6 +2511,15 @@ "api-reference/endpoints/certificate-profiles/get-latest-active-bundle" ] }, + { + "group": "Certificate Alerts", + "pages": [ + "api-reference/endpoints/pki-alerts/create", + "api-reference/endpoints/pki-alerts/read", + "api-reference/endpoints/pki-alerts/update", + "api-reference/endpoints/pki-alerts/delete" + ] + }, { "group": "Certificate Syncs", "pages": [ diff --git a/docs/documentation/getting-started/concepts/client-integrations.mdx b/docs/documentation/getting-started/concepts/client-integrations.mdx index bcd935830..aa7b37d4a 100644 --- a/docs/documentation/getting-started/concepts/client-integrations.mdx +++ b/docs/documentation/getting-started/concepts/client-integrations.mdx @@ -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. diff --git a/docs/documentation/guides/nextjs-vercel.mdx b/docs/documentation/guides/nextjs-vercel.mdx index ecefb0f2e..d0cdd639e 100644 --- a/docs/documentation/guides/nextjs-vercel.mdx +++ b/docs/documentation/guides/nextjs-vercel.mdx @@ -183,32 +183,7 @@ At this stage, you know how to use the Infisical CLI to inject secrets into your ## Infisical-Vercel integration for production environment variables -We'll now use the Infisical-Vercel integration send secrets from Infisical to Vercel as production environment variables. - -### Infisical-Vercel integration - -To begin we have to import the Next.js app into Vercel as a project. [Follow these instructions](https://vercel.com/docs/frameworks/nextjs) to deploy the Next.js app to Vercel. - -Next, navigate to your project's integrations tab in Infisical and press on the Vercel tile to grant Infisical access to your Vercel account. - -![integrations](../../images/integrations.png) - -![integrations vercel authorization](../../images/integrations/vercel/integrations-vercel-auth.png) - - - Opting in for the Infisical-Vercel integration will break end-to-end encryption since Infisical will be able to read - your secrets. This is, however, necessary for Infisical to sync the secrets to Vercel. - - Your secrets remain encrypted at rest following our [security guide mechanics](/internals/security). - - -Now select **Production** for (the source) **Environment** and sync it to the **Production Environment** of the (target) application in Vercel. -Lastly, press create integration to start syncing secrets to Vercel. - -![integrations vercel](../../images/integrations/vercel/integrations-vercel-create.png) -![integrations vercel](../../images/integrations/vercel/integrations-vercel.png) - -You should now see your secret from Infisical appear as production environment variables in your Vercel project. +Use our [Vercel Secret Syncs](../../integrations/secret-syncs/vercel) guide to sync secrets from Infisical to Vercel as production environment variables. At this stage, you know how to use the Infisical-Vercel integration to sync production secrets from Infisical to Vercel. @@ -245,4 +220,4 @@ At this stage, you know how to use the Infisical-Vercel integration to sync prod See also: - [Documentation for the Infisical CLI](/cli/overview) -- [Documentation for the Vercel integration](/integrations/cloud/vercel) +- [Documentation for the Vercel Secret Sync](../../integrations/secret-syncs/vercel) diff --git a/docs/documentation/platform/integrations.mdx b/docs/documentation/platform/integrations.mdx deleted file mode 100644 index 2414c5c14..000000000 --- a/docs/documentation/platform/integrations.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: "Integrations" -description: "How to sync your secrets among various 3rd-party services with Infisical." ---- - -Integrations allow environment variables to be synced across your entire infrastructure from local development to CI/CD and production. - - - View all available integrations and their guides - - -![integrations](../../images/integrations.png) diff --git a/docs/documentation/platform/pki/ca/acme-ca.mdx b/docs/documentation/platform/pki/ca/acme-ca.mdx index 774590c73..7f2290ff6 100644 --- a/docs/documentation/platform/pki/ca/acme-ca.mdx +++ b/docs/documentation/platform/pki/ca/acme-ca.mdx @@ -17,7 +17,7 @@ their **ACME Directory URL** such as: - ZeroSSL: `https://acme.zerossl.com/v2/DV90`. - SSL.com: `https://acme.ssl.com/sslcom-dv-rsa`. -When Infisical requests a certificate from an ACME-compatible CA, it creates a TXT record at `_acme-challenge.{your-domain}` in your configured DNS provider (e.g. Route53, Cloudflare, etc.); this TXT record contains the challenge token issued by the ACME-compatible CA to validate domain control for the requested certificate. +When Infisical requests a certificate from an ACME-compatible CA, it creates a TXT record at `_acme-challenge.{your-domain}` in your configured DNS provider (e.g. Route53, Cloudflare, DNS Made Easy, etc.); this TXT record contains the challenge token issued by the ACME-compatible CA to validate domain control for the requested certificate. The ACME provider checks for the existence of this TXT record to verify domain control before issuing the certificate back to Infisical. After validation completes successfully, Infisical automatically removes the TXT record from your DNS provider. @@ -120,6 +120,11 @@ In the following steps, we explore how to connect Infisical to an ACME-compatibl For detailed instructions on setting up a Cloudflare connection, see the [Cloudflare Connection](/integrations/app-connections/cloudflare) documentation. + + Navigate to your Certificate Management Project > App Connections and create a new DNS Made Easy connection. + + For detailed instructions on setting up a DNS Made Easy connection, see the [DNS Made Easy Connection](/integrations/app-connections/dns-made-easy) documentation. + @@ -153,7 +158,7 @@ In the following steps, we explore how to connect Infisical to an ACME-compatibl ### Sample request ```bash Request - curl 'https://app.infisical.com/api/v1/pki/ca/acme' \ + curl 'https://app.infisical.com/api/v1/cert-manager/ca/acme' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ --data-raw '{ diff --git a/docs/documentation/platform/pki/ca/private-ca.mdx b/docs/documentation/platform/pki/ca/private-ca.mdx index 74913d4cc..67b38b455 100644 --- a/docs/documentation/platform/pki/ca/private-ca.mdx +++ b/docs/documentation/platform/pki/ca/private-ca.mdx @@ -122,7 +122,7 @@ consisting of an (optional) root CA and an intermediate CA. ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/ca/internal' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -155,7 +155,7 @@ consisting of an (optional) root CA and an intermediate CA. ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/ca/internal' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -183,7 +183,7 @@ consisting of an (optional) root CA and an intermediate CA. ### Sample request ```bash Request - curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca//csr' \ + curl --location --request GET 'https://app.infisical.com/api/v1/cert-manager/ca/internal//csr' \ --header 'Authorization: Bearer ' \ --data-raw '' ``` @@ -204,7 +204,7 @@ consisting of an (optional) root CA and an intermediate CA. ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca//sign-intermediate' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/ca/internal//sign-intermediate' \ --header 'Content-Type: application/json' \ --data-raw '{ "csr": "", @@ -234,7 +234,7 @@ consisting of an (optional) root CA and an intermediate CA. ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca//import-certificate' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/ca/internal//import-certificate' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -292,7 +292,7 @@ the certificate back to the intermediate CA. ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca//renew' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/ca/internal//renew' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ diff --git a/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager.mdx b/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager.mdx index e2cad7498..22285be8e 100644 --- a/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager.mdx +++ b/docs/documentation/platform/pki/certificate-syncs/aws-certificate-manager.mdx @@ -70,7 +70,7 @@ These permissions allow Infisical to list, import, tag, and manage certificates ```bash Request curl --request POST \ - --url https://app.infisical.com/api/v1/pki/syncs/aws-certificate-manager \ + --url https://app.infisical.com/api/v1/cert-manager/syncs/aws-certificate-manager \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ diff --git a/docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx b/docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx index 26472ce44..86461bb93 100644 --- a/docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx +++ b/docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx @@ -102,7 +102,7 @@ Any role with these permissions would work such as a custom policy with **Secret ```bash Request curl --request POST \ - --url https://app.infisical.com/api/v1/pki/syncs/aws-secrets-manager \ + --url https://app.infisical.com/api/v1/cert-manager/syncs/aws-secrets-manager \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -140,7 +140,7 @@ Any role with these permissions would work such as a custom policy with **Secret ```bash Request curl --request POST \ - --url https://app.infisical.com/api/v1/pki/syncs/aws-secrets-manager \ + --url https://app.infisical.com/api/v1/cert-manager/syncs/aws-secrets-manager \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ diff --git a/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx b/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx index 135c74112..4c6c81bc0 100644 --- a/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx +++ b/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx @@ -77,7 +77,7 @@ Any role with these permissions would work such as the **Key Vault Certificates ```bash Request curl --request POST \ - --url https://app.infisical.com/api/v1/pki/syncs/azure-key-vault \ + --url https://app.infisical.com/api/v1/cert-manager/syncs/azure-key-vault \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ diff --git a/docs/documentation/platform/pki/certificate-syncs/chef.mdx b/docs/documentation/platform/pki/certificate-syncs/chef.mdx index 506a2c76a..ec3eedafd 100644 --- a/docs/documentation/platform/pki/certificate-syncs/chef.mdx +++ b/docs/documentation/platform/pki/certificate-syncs/chef.mdx @@ -103,7 +103,7 @@ Any role with these permissions would work such as a custom role with **Data Bag ```bash Request curl --request POST \ - --url https://app.infisical.com/api/v1/pki/syncs/chef \ + --url https://app.infisical.com/api/v1/cert-manager/syncs/chef \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -140,7 +140,7 @@ Any role with these permissions would work such as a custom role with **Data Bag ```bash Request curl --request POST \ - --url https://app.infisical.com/api/v1/pki/syncs/chef \ + --url https://app.infisical.com/api/v1/cert-manager/syncs/chef \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index de8de4541..da73de37d 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -221,7 +221,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v3/pki/certificates/issue-certificate' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/certificates/issue-certificate' \ --header 'Content-Type: application/json' \ --data-raw '{ "profileId": "", @@ -260,7 +260,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/sign-certificate' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/certificates/sign-certificate' \ --header 'Content-Type: application/json' \ --data-raw '{ "certificateTemplateId": "", @@ -337,7 +337,7 @@ openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates//revoke' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/certificates//revoke' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -362,7 +362,7 @@ openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem ### Sample request ```bash Request - curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca//crls' \ + curl --location --request GET 'https://app.infisical.com/api/v1/cert-manager/ca/internal//crls' \ --header 'Authorization: Bearer ' ``` diff --git a/docs/documentation/platform/pki/certificates/certificates.mdx b/docs/documentation/platform/pki/certificates/certificates.mdx index abe198750..eab06fe43 100644 --- a/docs/documentation/platform/pki/certificates/certificates.mdx +++ b/docs/documentation/platform/pki/certificates/certificates.mdx @@ -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 @@ -220,7 +222,7 @@ openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates//revoke' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/certificates//revoke' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -245,7 +247,7 @@ openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem ### Sample request ```bash Request - curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca//crls' \ + curl --location --request GET 'https://app.infisical.com/api/v1/cert-manager/ca/internal//crls' \ --header 'Authorization: Bearer ' ``` diff --git a/docs/documentation/platform/pki/certificates/profiles.mdx b/docs/documentation/platform/pki/certificates/profiles.mdx index ccbef89cd..1121437cf 100644 --- a/docs/documentation/platform/pki/certificates/profiles.mdx +++ b/docs/documentation/platform/pki/certificates/profiles.mdx @@ -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. diff --git a/docs/documentation/platform/pki/enrollment-methods/acme.mdx b/docs/documentation/platform/pki/enrollment-methods/acme.mdx index 12c4779b5..3c12a5040 100644 --- a/docs/documentation/platform/pki/enrollment-methods/acme.mdx +++ b/docs/documentation/platform/pki/enrollment-methods/acme.mdx @@ -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/). @@ -47,7 +47,7 @@ In the following steps, we explore how to issue a X.509 certificate using the AC ```bash sudo certbot certonly \ --standalone \ - --server "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" \ + --server "https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory" \ --eab-kid "your-eab-kid" \ --eab-hmac-key "your-eab-secret" \ -d example.infisical.com \ diff --git a/docs/documentation/platform/pki/enrollment-methods/api.mdx b/docs/documentation/platform/pki/enrollment-methods/api.mdx index 304bafefc..bfbac7f2e 100644 --- a/docs/documentation/platform/pki/enrollment-methods/api.mdx +++ b/docs/documentation/platform/pki/enrollment-methods/api.mdx @@ -61,7 +61,7 @@ Here, select the certificate profile from step 1 that will be used to issue the ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificate-profiles' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/certificate-profiles' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -105,7 +105,7 @@ Here, select the certificate profile from step 1 that will be used to issue the ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v3/pki/certificates/issue-certificate' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/certificates/issue-certificate' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -151,7 +151,7 @@ Here, select the certificate profile from step 1 that will be used to issue the ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v3/pki/certificates/sign-certificate' \ + curl --location --request POST 'https://app.infisical.com/api/v1/cert-manager/certificates/sign-certificate' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ diff --git a/docs/documentation/platform/pki/integration-guides/apache-certbot.mdx b/docs/documentation/platform/pki/integration-guides/apache-certbot.mdx index 78f0301e1..23aed363a 100644 --- a/docs/documentation/platform/pki/integration-guides/apache-certbot.mdx +++ b/docs/documentation/platform/pki/integration-guides/apache-certbot.mdx @@ -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. @@ -29,7 +29,7 @@ Before you begin, make sure you have: From the ACME configuration, gather the following values: - - ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`. + - ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory`. - EAB Key Identifier (KID): A unique identifier that tells Infisical which ACME account is making the request. - EAB Secret: A secret key that authenticates your ACME client with Infisical. @@ -56,7 +56,7 @@ Before you begin, make sure you have: ```bash sudo certbot certonly \ --apache \ - --server "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" \ + --server "https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory" \ --eab-kid "your-eab-key-identifier" \ --eab-hmac-key "your-eab-secret" \ -d example.infisical.com \ @@ -182,4 +182,5 @@ Before you begin, make sure you have: - \ No newline at end of file + + diff --git a/docs/documentation/platform/pki/integration-guides/gloo-mesh.mdx b/docs/documentation/platform/pki/integration-guides/gloo-mesh.mdx index d1f1273fd..d83e062fa 100644 --- a/docs/documentation/platform/pki/integration-guides/gloo-mesh.mdx +++ b/docs/documentation/platform/pki/integration-guides/gloo-mesh.mdx @@ -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. diff --git a/docs/documentation/platform/pki/integration-guides/jboss-certbot.mdx b/docs/documentation/platform/pki/integration-guides/jboss-certbot.mdx index c0e1c896b..e8d8fb8b9 100644 --- a/docs/documentation/platform/pki/integration-guides/jboss-certbot.mdx +++ b/docs/documentation/platform/pki/integration-guides/jboss-certbot.mdx @@ -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. @@ -30,7 +30,7 @@ Before you begin, make sure you have: From the ACME configuration, gather the following values: - - ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`. + - ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory`. - EAB Key Identifier (KID): A unique identifier that tells Infisical which ACME account is making the request. - EAB Secret: A secret key that authenticates your ACME client with Infisical. @@ -67,7 +67,7 @@ Before you begin, make sure you have: ```bash sudo certbot certonly \ --standalone \ - --server "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" \ + --server "https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory" \ --eab-kid "your-eab-key-identifier" \ --eab-hmac-key "your-eab-secret" \ -d example.infisical.com \ @@ -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. - \ No newline at end of file + + diff --git a/docs/documentation/platform/pki/integration-guides/nginx-certbot.mdx b/docs/documentation/platform/pki/integration-guides/nginx-certbot.mdx index f28e5ee09..ca3c35034 100644 --- a/docs/documentation/platform/pki/integration-guides/nginx-certbot.mdx +++ b/docs/documentation/platform/pki/integration-guides/nginx-certbot.mdx @@ -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). @@ -29,7 +29,7 @@ Before you begin, make sure you have: From the ACME configuration, gather the following values: - - ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`. + - ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory`. - EAB Key Identifier (KID): A unique identifier that tells Infisical which ACME account is making the request. - EAB Secret: A secret key that authenticates your ACME client with Infisical. @@ -56,7 +56,7 @@ Before you begin, make sure you have: ```bash sudo certbot certonly \ --nginx \ - --server "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" \ + --server "https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory" \ --eab-kid "your-eab-key-identifier" \ --eab-hmac-key "your-eab-secret" \ -d example.infisical.com \ diff --git a/docs/documentation/platform/pki/integration-guides/tomcat-certbot.mdx b/docs/documentation/platform/pki/integration-guides/tomcat-certbot.mdx index ffb07bf1b..42e2b11ea 100644 --- a/docs/documentation/platform/pki/integration-guides/tomcat-certbot.mdx +++ b/docs/documentation/platform/pki/integration-guides/tomcat-certbot.mdx @@ -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. @@ -29,7 +29,7 @@ Before you begin, make sure you have: From the ACME configuration, gather the following values: - - ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`. + - ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory`. - EAB Key Identifier (KID): A unique identifier that tells Infisical which ACME account is making the request. - EAB Secret: A secret key that authenticates your ACME client with Infisical. @@ -64,7 +64,7 @@ Before you begin, make sure you have: ```bash sudo certbot certonly \ --standalone \ - --server "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" \ + --server "https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory" \ --eab-kid "your-eab-key-identifier" \ --eab-hmac-key "your-eab-secret" \ -d example.infisical.com \ @@ -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. - \ No newline at end of file + + diff --git a/docs/documentation/platform/pki/integration-guides/windows-server-acme.mdx b/docs/documentation/platform/pki/integration-guides/windows-server-acme.mdx index 2aab0870d..ae835d8a3 100644 --- a/docs/documentation/platform/pki/integration-guides/windows-server-acme.mdx +++ b/docs/documentation/platform/pki/integration-guides/windows-server-acme.mdx @@ -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. @@ -28,7 +28,7 @@ Before you begin, make sure you have: From the ACME configuration, gather the following values: - - ACME Directory URL: The URL that win-acme will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`. + - ACME Directory URL: The URL that win-acme will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory`. - EAB Key Identifier (KID): A unique identifier that tells Infisical which ACME account is making the request. - EAB Secret: A secret key that authenticates your ACME client with Infisical. @@ -67,7 +67,7 @@ Before you begin, make sure you have: Run the following win-acme command to request a certificate from Infisical: ```powershell - wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store pemfiles --pemfilespath "C:\certificates" --verbose + wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store pemfiles --pemfilespath "C:\certificates" --verbose ``` For guidance on each parameter: @@ -87,7 +87,7 @@ Before you begin, make sure you have: Replace the placeholder values with your actual configuration: - `example.infisical.com`: Your actual domain name - - `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`: Your Infisical ACME endpoint from Step 1 + - `https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory`: Your Infisical ACME endpoint from Step 1 - `your-eab-key-identifier` and `your-eab-secret`: Your External Account Binding credentials from Step 1 - `C:\certificates`: Your desired certificate storage location @@ -101,21 +101,21 @@ Before you begin, make sure you have: Store certificates directly in the [Windows Certificate Store](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/certificate-stores) for integration with IIS and other Windows services: ```powershell - wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store certificatestore --verbose + wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store certificatestore --verbose ``` Generate [PFX files](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/certutil) with password protection for easy deployment across Windows environments: ```powershell - wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store pfxfile --pfxfilepath "C:\certificates" --pfxpassword "your-secure-password" --verbose + wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store pfxfile --pfxfilepath "C:\certificates" --pfxpassword "your-secure-password" --verbose ``` For IIS Central SSL store integration in high-scale environments: ```powershell - wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store centralssl --centralsslstore "C:\CentralSSL" --verbose + wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store centralssl --centralsslstore "C:\CentralSSL" --verbose ``` @@ -129,7 +129,7 @@ Before you begin, make sure you have: Include the `--setuptaskscheduler` parameter in your initial command to automatically create the renewal task: ```powershell - wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store pemfiles --pemfilespath "C:\certificates" --setuptaskscheduler --verbose + wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/cert-manager/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store pemfiles --pemfilespath "C:\certificates" --setuptaskscheduler --verbose ``` **Option 2: Test manual renewal** @@ -191,4 +191,5 @@ Before you begin, make sure you have: + diff --git a/docs/documentation/platform/pki/k8s-cert-manager.mdx b/docs/documentation/platform/pki/k8s-cert-manager.mdx new file mode 100644 index 000000000..b0f696ba9 --- /dev/null +++ b/docs/documentation/platform/pki/k8s-cert-manager.mdx @@ -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. + + + + + 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. + + + 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. + + + + + + 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 + ``` + + + + 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. + + + + ```bash + kubectl create secret generic infisical-acme-eab-secret \ + --namespace \ + --from-literal=eabSecret= + ``` + + + ```yaml acme-eab-secret.yaml + apiVersion: v1 + kind: Secret + metadata: + name: infisical-acme-eab-secret + namespace: + data: + eabSecret: + ``` + + ```bash + kubectl apply -f acme-eab-secret.yaml + ``` + + + + + + Next, create a cert-manager `Issuer` (or `ClusterIssuer`) by replacing the placeholders ``, ``, and `` 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: + spec: + acme: + # ACME server URL from your Infisical certificate profile (Step 1) + server: + # Email address for ACME account + # (any valid email works; currently ignored by Infisical) + email: + externalAccountBinding: + # EAB Key ID from Step 1 + keyID: + # 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 -o wide + ``` + + ```bash + NAME AGE + issuer-infisical 21h + ``` + + + - 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/ + + + + + + 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: + 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 -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 + ``` + + + + 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 + ``` + + ```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. + + + + + +## FAQ + + + + 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). + + + Currently, not all fields are supported by the Infisical PKI ACME server. + + + + + 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). + + + diff --git a/docs/documentation/platform/pki/pki-issuer.mdx b/docs/documentation/platform/pki/pki-issuer.mdx deleted file mode 100644 index a1d07c98b..000000000 --- a/docs/documentation/platform/pki/pki-issuer.mdx +++ /dev/null @@ -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. - - - - - 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. - - - 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. - - - - 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 - ``` - - - Install the Infisical PKI Issuer controller into your Kubernetes cluster using one of the following methods: - - - - ```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 - ``` - - - ```bash - kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml - ``` - - - - - 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. - - - - ```bash - kubectl create secret generic issuer-infisical-client-secret \ - --namespace \ - --from-literal=clientSecret= - ``` - - - ```yaml secret-issuer.yaml - apiVersion: v1 - kind: Secret - metadata: - name: issuer-infisical-client-secret - namespace: - data: - clientSecret: - ``` - - ```bash - kubectl apply -f secret-issuer.yaml - ``` - - - - - 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: - spec: - url: "https://app.infisical.com" # the URL of your Infisical instance - projectId: # the ID of the project you want to use to issue certificates - certificateTemplateName: # the name of the certificate template you want to use to issue certificates against - authentication: - universalAuth: - clientId: # 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 -o wide - ``` - - ```bash - NAME AGE - issuer-infisical 21h - ``` - - - 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/). - - - - 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/). - - - - 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: - 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 -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 - ``` - - - 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 - ``` - - ```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. - - - - -## FAQ - - - - 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). - - - Currently, not all fields are supported by the Infisical PKI Issuer. - - - - - 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). - - - - 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. - - diff --git a/docs/images/app-connections/dns-made-easy/copy-api-credentials.png b/docs/images/app-connections/dns-made-easy/copy-api-credentials.png new file mode 100644 index 000000000..557085294 Binary files /dev/null and b/docs/images/app-connections/dns-made-easy/copy-api-credentials.png differ diff --git a/docs/images/app-connections/dns-made-easy/dns-made-easy-app-connection-created.png b/docs/images/app-connections/dns-made-easy/dns-made-easy-app-connection-created.png new file mode 100644 index 000000000..41d6fd90d Binary files /dev/null and b/docs/images/app-connections/dns-made-easy/dns-made-easy-app-connection-created.png differ diff --git a/docs/images/app-connections/dns-made-easy/dns-made-easy-app-connection-form.png b/docs/images/app-connections/dns-made-easy/dns-made-easy-app-connection-form.png new file mode 100644 index 000000000..0e6599231 Binary files /dev/null and b/docs/images/app-connections/dns-made-easy/dns-made-easy-app-connection-form.png differ diff --git a/docs/images/app-connections/dns-made-easy/dns-made-easy-app-connection-select.png b/docs/images/app-connections/dns-made-easy/dns-made-easy-app-connection-select.png new file mode 100644 index 000000000..28f67fcf2 Binary files /dev/null and b/docs/images/app-connections/dns-made-easy/dns-made-easy-app-connection-select.png differ diff --git a/docs/images/app-connections/dns-made-easy/generate-new-api-credentials.png b/docs/images/app-connections/dns-made-easy/generate-new-api-credentials.png new file mode 100644 index 000000000..de56f75fd Binary files /dev/null and b/docs/images/app-connections/dns-made-easy/generate-new-api-credentials.png differ diff --git a/docs/images/app-connections/dns-made-easy/nav-to-account-info.png b/docs/images/app-connections/dns-made-easy/nav-to-account-info.png new file mode 100644 index 000000000..56094c55e Binary files /dev/null and b/docs/images/app-connections/dns-made-easy/nav-to-account-info.png differ diff --git a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png index 9cb703e12..3a33e4351 100644 Binary files a/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png and b/docs/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png differ diff --git a/docs/images/platform/pki/certificate/cert-profile-modal.png b/docs/images/platform/pki/certificate/cert-profile-modal.png index 29280d01c..961ad466a 100644 Binary files a/docs/images/platform/pki/certificate/cert-profile-modal.png and b/docs/images/platform/pki/certificate/cert-profile-modal.png differ diff --git a/docs/integrations/app-connections/dns-made-easy.mdx b/docs/integrations/app-connections/dns-made-easy.mdx new file mode 100644 index 000000000..f2fe297bf --- /dev/null +++ b/docs/integrations/app-connections/dns-made-easy.mdx @@ -0,0 +1,59 @@ +--- +title: "DNS Made Easy" +description: "Learn how to configure a DNS Made Easy Connection for Infisical." +--- + +Infisical supports connecting to DNS Made Easy using API key and secret key for secure access to your DNS Made Easy service. + +## Configure API key and secret Key for Infisical + + + + Navigate to your DNS Made Easy dashboard and go to **Account Information** under the **Config** top menu. + + ![Navigate to Account Information](/images/app-connections/dns-made-easy/nav-to-account-info.png) + + If your **API Key** and **Secret Key** are already available, proceed to step 2. + + Otherwise, check the **Generate New API Credentials** then click the **Save** button to generate the new API credentials. + + ![Generate API Credentials](/images/app-connections/dns-made-easy/generate-new-api-credentials.png) + + + + After creation, copy your API key and secret key. + + ![Generated API Token](/images/app-connections/dns-made-easy/copy-api-credentials.png) + + + Keep your API key and secret key secure and do not share it. + Anyone with access to this token can manage your DNS Made Easy resources. + + + + + +## Setup DNS Made Easy Connection in Infisical + + + + Navigate to the **App Connections** page in the desired project. ![App + Connections Tab](/images/app-connections/general/add-connection.png) + + + Select the **DNS Made Easy Connection** option from the connection options + modal. ![Select DNS Made Easy + Connection](/images/app-connections/dns-made-easy/dns-made-easy-app-connection-select.png) + + + Enter your DNS Made Easy API key and secret key in the provided fields and + click **Connect to DNS Made Easy** to establish the connection. ![Connect to + DNS Made + Easy](/images/app-connections/dns-made-easy/dns-made-easy-app-connection-form.png) + + + Your **DNS Made Easy Connection** is now available for use in your Infisical + projects. ![DNS Made Easy Connection + Created](/images/app-connections/dns-made-easy/dns-made-easy-app-connection-created.png) + + diff --git a/docs/integrations/app-connections/gitlab.mdx b/docs/integrations/app-connections/gitlab.mdx index c9af952a7..588a6f990 100644 --- a/docs/integrations/app-connections/gitlab.mdx +++ b/docs/integrations/app-connections/gitlab.mdx @@ -12,6 +12,8 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access Using the GitLab Connection with OAuth on a self-hosted instance of Infisical requires configuring an OAuth application in GitLab and registering your instance with it. + If you're self-hosting GitLab with custom certificates, you will have to configure your Infisical instance to trust these certificates. To learn how, please follow [this guide](../../self-hosting/guides/custom-certificates). + **Prerequisites:** - A GitLab account with existing projects - Self-hosted Infisical instance diff --git a/docs/integrations/app-connections/overview.mdx b/docs/integrations/app-connections/overview.mdx index 8b1032e7d..1201ca3ca 100644 --- a/docs/integrations/app-connections/overview.mdx +++ b/docs/integrations/app-connections/overview.mdx @@ -75,10 +75,6 @@ to limit the access of this entity to the minimal permission set required to per 4. Utilize the Connection: Use your App Connection for various features across Infisical such as our Secrets Sync by selecting it via the dropdown menu in the UI or by passing the associated `connectionId` when generating resources via the API. - - Infisical is continuously expanding its third-party application support. If your desired application isn't listed, - you can still use previous methods of connecting to it such as our Native Integrations. - ## Platform Managed Credentials diff --git a/docs/integrations/cloud/aws-amplify.mdx b/docs/integrations/cicd/aws-amplify.mdx similarity index 95% rename from docs/integrations/cloud/aws-amplify.mdx rename to docs/integrations/cicd/aws-amplify.mdx index 6d3123b10..28de7640c 100644 --- a/docs/integrations/cloud/aws-amplify.mdx +++ b/docs/integrations/cicd/aws-amplify.mdx @@ -19,7 +19,7 @@ This approach enables you to fetch secrets from Infisical during Amplify build t - Create a machine identtiy and connect it to your Infisical project. You can read more about how to use machine identities [here](/documentation/platform/identities/machine-identities). The machine identity will allow you to authenticate and fetch secrets from Infisical. + Create a machine identity and connect it to your Infisical project. You can read more about how to use machine identities [here](/documentation/platform/identities/machine-identities). The machine identity will allow you to authenticate and fetch secrets from Infisical. @@ -108,7 +108,7 @@ This approach enables you to fetch secrets from Infisical during Amplify build t - Follow the [Infisical AWS SSM Parameter Store Integration Guide](./aws-parameter-store) to set up the integration. Pause once you reach the step where it asks you to select the path you would like to sync. + Follow the [Infisical AWS SSM Parameter Store Secret Syncs Guide](../secret-syncs/aws-parameter-store) to set up the integration. Pause once you reach the step where it asks you to select the path you would like to sync. ![amplify app id](../../images/integrations/aws/integrations-amplify-app-id.png) diff --git a/docs/integrations/cicd/bitbucket.mdx b/docs/integrations/cicd/bitbucket.mdx index 3c1330308..44893a5b6 100644 --- a/docs/integrations/cicd/bitbucket.mdx +++ b/docs/integrations/cicd/bitbucket.mdx @@ -12,29 +12,7 @@ Prerequisites: - - - Navigate to your project's integrations tab in Infisical. - - ![integrations](/images/integrations.png) - - Press on the Bitbucket tile and grant Infisical access to your Bitbucket account. - - ![integrations bitbucket authorization](/images/integrations/bitbucket/integrations-bitbucket.png) - - - Select which workspace, repository, and optionally, deployment environment, you'd like to sync your secrets - to. - ![integrations configure - bitbucket](/images/integrations/bitbucket/integrations-bitbucket-configuration.png) - - Once created, your integration will begin syncing secrets to the configured repository or deployment - environment. - - ![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket.png) - - - + Use our [Bitbucket Secret Syncs](../secret-syncs/bitbucket) diff --git a/docs/integrations/cicd/circleci.mdx b/docs/integrations/cicd/circleci.mdx deleted file mode 100644 index 5bf04822d..000000000 --- a/docs/integrations/cicd/circleci.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: "CircleCI" -description: "How to sync secrets from Infisical to CircleCI" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain an API token in User Settings > Personal API Tokens - - ![integrations circleci token](/images/integrations/circleci/integrations-circleci-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](/images/integrations.png) - - Press on the CircleCI tile and input your CircleCI API token to grant Infisical access to your CircleCI account. - - ![integrations circleci authorization](/images/integrations/circleci/integrations-circleci-auth.png) - - - - Select which Infisical environment secrets you want to sync to which CircleCI project or context. - - - ![integrations circle ci project](/images/integrations/circleci/integrations-circleci-create-project.png) - - - ![integrations circle ci project](/images/integrations/circleci/integrations-circleci-create-context.png) - - - - Finally, press create integration to start syncing secrets to CircleCI. - ![integrations circleci](/images/integrations/circleci/integrations-circleci.png) - - - diff --git a/docs/integrations/cicd/codefresh.mdx b/docs/integrations/cicd/codefresh.mdx deleted file mode 100644 index cf69ae04d..000000000 --- a/docs/integrations/cicd/codefresh.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: "Codefresh" -description: "How to sync secrets from Infisical to Codefresh" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain an API key in User Settings > API Keys - - ![integrations codefresh dashboard](../../images/integrations/codefresh/integrations-codefresh-dashboard.png) - ![integrations codefresh token](../../images/integrations/codefresh/integrations-codefresh-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Codefresh tile and input your Codefresh API key to grant Infisical access to your Codefresh account. - - ![integrations codefresh authorization](../../images/integrations/codefresh/integrations-codefresh-auth.png) - - - - Select which Infisical environment secrets you want to sync to which Codefresh service and press create integration to start syncing secrets to Codefresh. - - ![create integration codefresh](../../images/integrations/codefresh/integrations-codefresh-create.png) - ![integrations codefresh](../../images/integrations/codefresh/integrations-codefresh.png) - - \ No newline at end of file diff --git a/docs/integrations/cicd/githubactions.mdx b/docs/integrations/cicd/githubactions.mdx index 82076874c..fa4387413 100644 --- a/docs/integrations/cicd/githubactions.mdx +++ b/docs/integrations/cicd/githubactions.mdx @@ -4,204 +4,6 @@ description: "How to sync secrets from Infisical to GitHub Actions" --- - Alternatively, you can use Infisical's official GitHub Action - [here](https://github.com/Infisical/secrets-action). - - -Infisical lets you sync secrets to GitHub at the organization-level, repository-level, and repository environment-level. - -## Connecting with GitHub App (Recommended) - - - - - - Navigate to your project's integrations tab in Infisical and press on the GitHub tile. - - ![integrations](../../images/integrations/github/app/integration-overview.png) - - Select GitHub App as the authentication method and click **Connect to GitHub**. - - ![integrations github app auth selection](../../images/integrations/github/app/github-app-method-selection.png) - - You will then be redirected to the GitHub app installation page. - - ![integrations github app installation](../../images/integrations/github/app/github-app-installation.png) - - Install and authorize the GitHub application. This will redirect you back to the Infisical integration page. - - - - Select which Infisical environment secrets you want to sync to which GitHub organization, repository, or repository environment. - - - - ![integrations github](../../images/integrations/github/integrations-github-scope-repo.png) - - - ![integrations github](../../images/integrations/github/integrations-github-scope-org.png) - - When using the organization scope, your secrets will be saved in the top-level of your GitHub Organization. - - You can choose the visibility, which defines which repositories can access the secrets. The options are: - - **All public repositories**: All public repositories in the organization can access the secrets. - - **All private repositories**: All private repositories in the organization can access the secrets. - - **Selected repositories**: Only the selected repositories can access the secrets. This gives a more fine-grained control over which repositories can access the secrets. You can select _both_ private and public repositories with this option. - - - ![integrations github](../../images/integrations/github/integrations-github-scope-env.png) - - - - Finally, press create integration to start syncing secrets to GitHub. - - ![integrations github](../../images/integrations/github/integrations-github.png) - - - - - - Using the GitHub integration with app authentication on a self-hosted instance of Infisical requires configuring an application on GitHub - and registering your instance with it. - - - Navigate to the GitHub app settings [here](https://github.com/settings/apps). Click **New GitHub App**. - - ![integrations github app create](../../images/integrations/github/app/self-hosted-github-app-create.png) - - Give the application a name, a homepage URL (your self-hosted domain i.e. `https://your-domain.com`), and a callback URL (i.e. `https://your-domain.com/integrations/github/oauth2/callback`). - - ![integrations github app basic details](../../images/integrations/github/app/self-hosted-github-app-basic-details.png) - - Enable request user authorization during app installation. - ![integrations github app enable auth](../../images/integrations/github/app/self-hosted-github-app-enable-oauth.png) - - Disable webhook by unchecking the Active checkbox. - ![integrations github app webhook](../../images/integrations/github/app/self-hosted-github-app-webhook.png) - - Set the repository permissions as follows: Metadata: Read-only, Secrets: Read and write, Environments: Read and write, Actions: Read. - ![integrations github app repository](../../images/integrations/github/app/self-hosted-github-app-repository.png) - - Similarly, set the organization permissions as follows: Secrets: Read and write. - ![integrations github app organization](../../images/integrations/github/app/self-hosted-github-app-organization.png) - - Create the Github application. - ![integrations github app create confirm](../../images/integrations/github/app/self-hosted-github-app-create-confirm.png) - - - If you have a GitHub organization, you can create an application under it - in your organization Settings > Developer settings > GitHub Apps > New GitHub App. - - - - Generate a new **Client Secret** for your GitHub application. - ![integrations github app create secret](../../images/integrations/github/app/self-hosted-github-app-secret.png) - - Generate a new **Private Key** for your Github application. - ![integrations github app create private key](../../images/integrations/github/app/self-hosted-github-app-private-key.png) - - Obtain the necessary Github application credentials. This would be the application slug, client ID, app ID, client secret, and private key. - ![integrations github app credentials](../../images/integrations/github/app/self-hosted-github-app-credentials.png) - - Back in your Infisical instance, add the five new environment variables for the credentials of your GitHub application: - - - `CLIENT_ID_GITHUB_APP`: The **Client ID** of your GitHub application. - - `CLIENT_SECRET_GITHUB_APP`: The **Client Secret** of your GitHub application. - - `CLIENT_SLUG_GITHUB_APP`: The **Slug** of your GitHub application. This is the one found in the URL. - - `CLIENT_APP_ID_GITHUB_APP`: The **App ID** of your GitHub application. - - `CLIENT_PRIVATE_KEY_GITHUB_APP`: The **Private Key** of your GitHub application. - - Once added, restart your Infisical instance and use the GitHub integration via app authentication. - - - - - - -## Connecting with GitHub OAuth - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) -- Ensure that you have admin privileges to the repository you want to sync secrets to. - - - - - - Navigate to your project's integrations tab in Infisical and press on the GitHub tile. - ![integrations](../../images/integrations/github/integration-overview.png) - - Select OAuth as the authentication method and click **Connect to GitHub**. - ![integrations github oauth auth selection](../../images/integrations/github/github-oauth-method-selection.png) - - Grant Infisical access to your GitHub account (organization and repo privileges). - ![integrations github authorization](../../images/integrations/github/integrations-github-auth.png) - - - - Select which Infisical environment secrets you want to sync to which GitHub organization, repository, or repository environment. - - - - ![integrations github](../../images/integrations/github/integrations-github-scope-repo.png) - - - ![integrations github](../../images/integrations/github/integrations-github-scope-org.png) - - When using the organization scope, your secrets will be saved in the top-level of your GitHub Organization. - - You can choose the visibility, which defines which repositories can access the secrets. The options are: - - **All public repositories**: All public repositories in the organization can access the secrets. - - **All private repositories**: All private repositories in the organization can access the secrets. - - **Selected repositories**: Only the selected repositories can access the secrets. This gives a more fine-grained control over which repositories can access the secrets. You can select _both_ private and public repositories with this option. - - - ![integrations github](../../images/integrations/github/integrations-github-scope-env.png) - - - - Finally, press create integration to start syncing secrets to GitHub. - - ![integrations github](../../images/integrations/github/integrations-github.png) - - - - - - Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub - and registering your instance with it. - - - Navigate to your user Settings > Developer settings > OAuth Apps to create a new GitHub OAuth application. - - ![integrations github config](../../images/integrations/github/integrations-github-config-settings.png) - ![integrations github config](../../images/integrations/github/integrations-github-config-dev-settings.png) - ![integrations github config](../../images/integrations/github/integrations-github-config-new-app.png) - - Create the OAuth application. As part of the form, set the **Homepage URL** to your self-hosted domain `https://your-domain.com` - and the **Authorization callback URL** to `https://your-domain.com/integrations/github/oauth2/callback`. - - ![integrations github config](../../images/integrations/github/integrations-github-config-new-app-form.png) - - - If you have a GitHub organization, you can create an OAuth application under it - in your organization Settings > Developer settings > OAuth Apps > New Org OAuth App. - - - - Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application. - - ![integrations github config](../../images/integrations/github/integrations-github-config-credentials.png) - - Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application: - - - `CLIENT_ID_GITHUB`: The **Client ID** of your GitHub OAuth application. - - `CLIENT_SECRET_GITHUB`: The **Client Secret** of your GitHub OAuth application. - - Once added, restart your Infisical instance and use the GitHub integration. - - - - - + Use our [GitHub Secret Syncs](../secret-syncs/github) to sync secrets to GitHub at the organization-level, repository-level, and repository environment-level. + Alternatively, you can use Infisical's official GitHub Action [here](https://github.com/Infisical/secrets-action). + \ No newline at end of file diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx index 2da61ef77..7cbf9512e 100644 --- a/docs/integrations/cicd/gitlab.mdx +++ b/docs/integrations/cicd/gitlab.mdx @@ -3,41 +3,13 @@ title: "GitLab" description: "How to sync secrets from Infisical to GitLab" --- - - + Prerequisites: - - Set up and add envars to [Infisical Cloud](https://app.infisical.com) + - Set up and add envars to [Infisical Cloud](https://app.infisical.com). - - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the GitLab tile and grant Infisical access to your GitLab account. - - ![integrations gitlab authorization](../../images/integrations/gitlab/integrations-gitlab-auth.png) - - - - Select which Infisical environment secrets you want to sync to which GitLab repository and press create integration to start syncing secrets to GitLab. - - ![integrations gitlab](../../images/integrations/gitlab/integrations-gitlab-create.png) - - Note that the GitLab integration supports a few options in the **Options** tab: - - - Secret Prefix: If inputted, the prefix is appended to the front of every secret name prior to being synced. - - Secret Suffix: If inputted, the suffix to appended to the back of every name of every secret prior to being synced. - - Setting a secret prefix or suffix ensures that existing secrets in GitLab are not overwritten during the sync. As part of this process, Infisical abstains from mutating any secrets in GitLab without the specified prefix or suffix. - - ![integrations gitlab options](../../images/integrations/gitlab/integrations-gitlab-create-options.png) - - ![integrations gitlab](../../images/integrations/gitlab/integrations-gitlab.png) - - + Use our [GitLab Secret Syncs](../secret-syncs/gitlab) @@ -70,42 +42,4 @@ description: "How to sync secrets from Infisical to GitLab" - - - - - Using the GitLab integration on a self-hosted instance of Infisical requires configuring an application in GitLab - and registering your instance with it. - If you're self-hosting Gitlab with custom certificates, you will have to configure your Infisical instance to trust these certificates. To learn how, please follow [this guide](../../self-hosting/guides/custom-certificates). - - - Navigate to your user Settings > Applications to create a new GitLab application. - - ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-edit-profile.png) - ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-new-app.png) - - Create the application. As part of the form, set the **Redirect URI** to `https://your-domain.com/integrations/gitlab/oauth2/callback`. - - ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-new-app-form.png) - - - If you have a GitLab group, you can create an OAuth application under it - in your group Settings > Applications. - - - - Obtain the **Application ID** and **Secret** for your GitLab application. - - ![integrations gitlab config](../../images/integrations/gitlab/integrations-gitlab-config-credentials.png) - - Back in your Infisical instance, add two new environment variables for the credentials of your GitLab application: - - - `CLIENT_ID_GITLAB`: The **Client ID** of your GitLab application. - - `CLIENT_SECRET_GITLAB`: The **Secret** of your GitLab application. - - Once added, restart your Infisical instance and use the GitLab integration. - - - - - + \ No newline at end of file diff --git a/docs/integrations/cicd/octopus-deploy.mdx b/docs/integrations/cicd/octopus-deploy.mdx deleted file mode 100644 index 90f06e09a..000000000 --- a/docs/integrations/cicd/octopus-deploy.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: "Octopus Deploy" -description: "Learn how to sync secrets from Infisical to Octopus Deploy" ---- - -Prerequisites: - -- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - - - - Navigate to **Configuration** > **Users** and click on the **Create Service Account** button. - - ![integrations octopus deploy - users](/images/integrations/octopus-deploy/integrations-octopus-deploy-user-settings.png) - - Fill out the required fields and click on the **Save** button. - ![integrations octopus deploy service - account](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-service-account.png) - - - On the **Service Account** user page, expand the **API Keys** section and click on the **New API Key** button. - - ![integrations octopus deploy - new api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-api-key.png) - - Fill out the required fields and click on the **Generate New** button. - - ![integrations octopus deploy - generate api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-generate-api-key.png) - - If you configure your access token to expire, - you will need to generate a new API key for Infisical prior to this date to keep your integration running. - - Copy the generated **API Key** and click on the **Close** button. - - ![integrations octopus deploy - copy api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-copy-api-key.png) - - - You can skip creating a new team if you already have an Octopus Deploy team configured with - the **Project Contributor** role to assign your Service Account to. - - Navigate to **Configuration** > **Teams** and click on the **Add Team** button. - - ![integrations octopus deploy - teams](/images/integrations/octopus-deploy/integrations-octopus-deploy-team-settings.png) - - Create a new team for **Service Accounts** and click on the **Save** button. - ![integrations octopus deploy add - team](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png) - - On the **Members** tab, click on the **Add Member** button, add your **Infisical Service Account** and click on the **Add** button. - ![integrations octopus deploy add service account to team](/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png) - - On the **User Roles** tab, click on the **Include User Role** button, and add the **Project Contributor** role. Optionally, - click on the **Define Scope** button to further refine what projects your Service Account has access to. Click on the **Apply** button once complete. - ![integrations octopus deploy add user roles to team](/images/integrations/octopus-deploy/integrations-octopus-deploy-add-role.png) - - Save your team changes by clicking on the **Save** button. - ![integrations octopus deploy save team changes](/images/integrations/octopus-deploy/integrations-octopus-deploy-save-team.png) - - - In Infisical, navigate to your **Project** > **Integrations** page and select the **Octopus Deploy** integration. - ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-integrations.png) - - Enter your **Instance URL** and **API Key** from **Octopus Deploy** to authorize Infisical. - ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-authorize.png) - - Select a **Space** and **Project** from **Octopus Deploy** to sync secrets to; configuring additional **Scope Values** as needed. Click on the **Create Integration** button once configured. - ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-create.png) - - Your Infisical secrets will begin to sync to **Octopus Deploy**. - ![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-sync.png) - - \ No newline at end of file diff --git a/docs/integrations/cicd/rundeck.mdx b/docs/integrations/cicd/rundeck.mdx deleted file mode 100644 index bda7d8162..000000000 --- a/docs/integrations/cicd/rundeck.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: "Rundeck" -description: "How to sync secrets from Infisical to Rundeck" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a User API Token in the Profile settings of Rundeck - - ![integrations rundeck token](../../images/integrations/rundeck/integrations-rundeck-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Rundeck tile and input your Rundeck instance Base URL and User API token to grant Infisical access to manage Rundeck keys - - ![integrations rundeck authorization](../../images/integrations/rundeck/integrations-rundeck-auth.png) - - - - Select which Infisical environment secrets you want to sync to a Rundeck Key Storage Path and press create integration to start syncing secrets to Rundeck. - - ![create integration rundeck](../../images/integrations/rundeck/integrations-rundeck-create.png) - ![integrations rundeck](../../images/integrations/rundeck/integrations-rundeck.png) - - - diff --git a/docs/integrations/cloud/teamcity.mdx b/docs/integrations/cicd/teamcity.mdx similarity index 100% rename from docs/integrations/cloud/teamcity.mdx rename to docs/integrations/cicd/teamcity.mdx diff --git a/docs/integrations/cicd/travisci.mdx b/docs/integrations/cicd/travisci.mdx deleted file mode 100644 index 873c371b6..000000000 --- a/docs/integrations/cicd/travisci.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Travis CI" -description: "How to sync secrets from Infisical to Travis CI" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain your API token in User Settings > API authentication > Token - - ![integrations travis ci token](../../images/integrations/travis-ci/integrations-travisci-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Travis CI tile and input your Travis CI API token to grant Infisical access to your Travis CI account. - - ![integrations travis ci authorization](../../images/integrations/travis-ci/integrations-travisci-auth.png) - - - - Select which Infisical environment secrets you want to sync to which Travis CI repository and press create integration to start syncing secrets to Travis CI. - - ![create integration travis ci](../../images/integrations/travis-ci/integrations-travisci-create.png) - ![integrations travis ci](../../images/integrations/travis-ci/integrations-travisci.png) - - \ No newline at end of file diff --git a/docs/integrations/cloud/aws-parameter-store.mdx b/docs/integrations/cloud/aws-parameter-store.mdx deleted file mode 100644 index d2bb36a0b..000000000 --- a/docs/integrations/cloud/aws-parameter-store.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "AWS Parameter Store" -description: "Learn how to sync secrets from Infisical to AWS Parameter Store." ---- - - - The AWS Parameter Store Native Integration will be deprecated in 2026. Please migrate to our new [AWS Parameter Store Sync](../secret-syncs/aws-parameter-store). - \ No newline at end of file diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx deleted file mode 100644 index a56461998..000000000 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "AWS Secrets Manager" -description: "Learn how to sync secrets from Infisical to AWS Secrets Manager." ---- - - - The AWS Secrets Manager Native Integration will be deprecated in 2026. Please migrate to our new [AWS Secrets Manager Sync](../secret-syncs/aws-secrets-manager). - \ No newline at end of file diff --git a/docs/integrations/cloud/azure-app-configuration.mdx b/docs/integrations/cloud/azure-app-configuration.mdx deleted file mode 100644 index 4e7dfd94f..000000000 --- a/docs/integrations/cloud/azure-app-configuration.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Azure App Configuration" -description: "How to sync secrets from Infisical to Azure App Configuration" ---- - - - The Azure App Configuration Native Integration will be deprecated in 2026. Please migrate to our new [Azure App Configuration Sync](../secret-syncs/azure-app-configuration). - \ No newline at end of file diff --git a/docs/integrations/cloud/azure-devops.mdx b/docs/integrations/cloud/azure-devops.mdx deleted file mode 100644 index 4eaaf0cc1..000000000 --- a/docs/integrations/cloud/azure-devops.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: "Azure DevOps" -description: "How to sync secrets from Infisical to Azure DevOps" ---- - -### Usage -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create a new [Azure DevOps](https://dev.azure.com) project if you don't have one already. - - -#### Create a new Azure DevOps personal access token (PAT) -You'll need to create a new personal access token (PAT) in order to authenticate Infisical with Azure DevOps. - - - ![integrations](../../images/integrations/azure-devops/overview-page.png) - - - Make sure the newly created token has Read/Write access to the Release scope. - ![integrations](../../images/integrations/azure-devops/create-new-token.png) - - - Please make sure that the token has access to the following scopes: Variable Groups _(read, create, & manage)_, Release _(read/write)_, Project and Team _(read)_, Service Connections _(read & query)_ - - - - Copy the newly created token as this will be used to authenticate Infisical with Azure DevOps. - ![integrations](../../images/integrations/azure-devops/new-token-created.png) - - - -#### Setup the Infisical Azure DevOps integration -Navigate to your project's integrations tab and select the 'Azure DevOps' integration. -![integrations](../../images/integrations.png) - - - - Enter your credentials that you obtained from the previous step. - - 1. Azure DevOps API token is the personal access token (PAT) you created in the previous step. - 2. Azure DevOps organization name is the name of your Azure DevOps organization. - - ![integrations](../../images/integrations/azure-devops/new-infiscial-integration-step-1.png) - - - Select Infisical project and secret path you want to sync into Azure DevOps. - Finally, press create integration to start syncing secrets to Azure DevOps. - - ![integrations](../../images/integrations/azure-devops/new-infiscial-integration-step-2.png) - - - -Now you have successfully integrated Infisical with Azure DevOps. Your existing and future secret changes will automatically sync to Azure DevOps. -You can view your secrets by navigating to your Azure DevOps project and selecting the 'Library' tab under 'Pipelines' in the 'Library' section. diff --git a/docs/integrations/cloud/azure-key-vault.mdx b/docs/integrations/cloud/azure-key-vault.mdx deleted file mode 100644 index b0bd80c63..000000000 --- a/docs/integrations/cloud/azure-key-vault.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Azure Key Vault" -description: "How to sync secrets from Infisical to Azure Key Vault" ---- - - - The Azure Key Vault Native Integration will be deprecated in 2026. Please migrate to our new [Azure Key Vault Sync](../secret-syncs/azure-key-vault). - \ No newline at end of file diff --git a/docs/integrations/cloud/checkly.mdx b/docs/integrations/cloud/checkly.mdx deleted file mode 100644 index 00ec38d2f..000000000 --- a/docs/integrations/cloud/checkly.mdx +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: "Checkly" -description: "How to sync secrets from Infisical to Checkly" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Checkly API Key in User Settings > API Keys. - - ![integrations checkly dashboard](../../images/integrations/checkly/integrations-checkly-dashboard.png) - ![integrations checkly token](../../images/integrations/checkly/integrations-checkly-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Checkly tile and input your Checkly API Key to grant Infisical access to your Checkly account. - - ![integrations checkly authorization](../../images/integrations/checkly/integrations-checkly-auth.png) - - - - Select which Infisical environment secrets you want to sync to Checkly and press create integration to start syncing secrets. - - ![integrations checkly](../../images/integrations/checkly/integrations-checkly-create.png) - - - Infisical integrates with Checkly's environment variables at the **global** and **group** levels. - - To sync secrets to a specific group, you can select a group from the Checkly Group dropdown; otherwise, leaving it empty will sync secrets globally. - - - ![integrations checkly](../../images/integrations/checkly/integrations-checkly.png) - - - In the new version of the Checkly integration, you are able to specify suffixes that depend on the secrets' environment and path. - If you choose to do so, you should utilize such suffixes for ALL Checkly integrations – otherwise the integration system - might run into issues with deleting secrets from the wrong environments. - - - \ No newline at end of file diff --git a/docs/integrations/cloud/cloud-66.mdx b/docs/integrations/cloud/cloud-66.mdx deleted file mode 100644 index c087f6564..000000000 --- a/docs/integrations/cloud/cloud-66.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: "Cloud 66" -description: "How to sync secrets from Infisical to Cloud 66" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - -## Navigate to your project's integrations tab - -![integrations](../../images/integrations.png) - -## Enter your Cloud 66 Access Token - -In Cloud 66 Dashboard, click on the top right icon > Account Settings > Access Token -![integrations cloud 66 dashboard](../../images/integrations/cloud-66/integrations-cloud-66-dashboard.png) -![integrations cloud 66 access token](../../images/integrations/cloud-66/integrations-cloud-66-access-token.png) - -Create new Personal Access Token. -![integrations cloud 66 personal access token](../../images/integrations/cloud-66/integrations-cloud-66-pat.png) - -Name it **infisical** and check **Public** and **Admin**. Then click "Create Token" -![integrations cloud 66 personal access token setup](../../images/integrations/cloud-66/integrations-cloud-66-pat-setup.png) - -Copy and save your token. -![integrations cloud 66 copy API token](../../images/integrations/cloud-66/integrations-cloud-66-copy-pat.png) - -### Go to Infisical Integration Page - -Click on the Cloud 66 tile and enter your API token to grant Infisical access to your Cloud 66 account. -![integrations cloud 66 tile in infisical dashboard](../../images/integrations/cloud-66/integrations-cloud-66-infisical-dashboard.png) - -Enter your Cloud 66 Personal Access Token here. Then click "Connect to Cloud 66". -![integrations cloud 66 tile in infisical dashboard](../../images/integrations/cloud-66/integrations-cloud-66-paste-pat.png) - - -## Start integration - -Select which Infisical environment secrets you want to sync to which Cloud 66 stacks and press create integration to start syncing secrets to Cloud 66. -![integrations laravel forge](../../images/integrations/cloud-66/integrations-cloud-66-create.png) - - - Any existing environment variables in Cloud 66 will be deleted when you start syncing. Make sure to add all the secrets into the Infisical dashboard first before doing any integrations. - - -Done! -![integrations laravel forge](../../images/integrations/cloud-66/integrations-cloud-66-done.png) diff --git a/docs/integrations/cloud/cloudflare-pages.mdx b/docs/integrations/cloud/cloudflare-pages.mdx deleted file mode 100644 index addba4fcd..000000000 --- a/docs/integrations/cloud/cloudflare-pages.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Cloudflare Pages" -description: "How to sync secrets from Infisical to Cloudflare Pages" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Cloudflare [API token](https://dash.cloudflare.com/profile/api-tokens) and [Account ID](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/): - - Create a new [API token](https://dash.cloudflare.com/profile/api-tokens) in My Profile > API Tokens - - ![integrations cloudflare credentials 1](../../images/integrations/cloudflare/integrations-cloudflare-credentials-1.png) - ![integrations cloudflare credentials 2](../../images/integrations/cloudflare/integrations-cloudflare-credentials-2.png) - ![integrations cloudflare credentials 3](../../images/integrations/cloudflare/integrations-cloudflare-credentials-3.png) - - Copy your [Account ID](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/) from Account > Workers & Pages > Overview - - ![integrations cloudflare credentials 4](../../images/integrations/cloudflare/integrations-cloudflare-credentials-4.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Cloudflare Pages tile and input your Cloudflare API token and account ID to grant Infisical access to your Cloudflare Pages. - - ![integrations cloudflare authorization](../../images/integrations/cloudflare/integrations-cloudflare-auth.png) - - - - Select which Infisical environment secrets you want to sync to Cloudflare and press create integration to start syncing secrets. - - ![integrations cloudflare](../../images/integrations/cloudflare/integrations-cloudflare-create.png) - ![integrations cloudflare](../../images/integrations/cloudflare/integrations-cloudflare.png) - - \ No newline at end of file diff --git a/docs/integrations/cloud/cloudflare-workers.mdx b/docs/integrations/cloud/cloudflare-workers.mdx deleted file mode 100644 index 10a579701..000000000 --- a/docs/integrations/cloud/cloudflare-workers.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Cloudflare Workers" -description: "How to sync secrets from Infisical to Cloudflare Workers" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Cloudflare [API token](https://dash.cloudflare.com/profile/api-tokens) and [Account ID](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/): - - Create a new [API token](https://dash.cloudflare.com/profile/api-tokens) in My Profile > API Tokens - - ![integrations cloudflare credentials 1](../../images/integrations/cloudflare/integrations-cloudflare-credentials-1.png) - ![integrations cloudflare credentials 2](../../images/integrations/cloudflare/integrations-cloudflare-credentials-2.png) - ![integrations cloudflare credentials 3](../../images/integrations/cloudflare/integrations-cloudflare-workers-permission.png) - - Copy your [Account ID](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/) from Account > Workers & Pages > Overview - - ![integrations cloudflare credentials 4](../../images/integrations/cloudflare/integrations-cloudflare-credentials-4.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Cloudflare Workers tile and input your Cloudflare API token and account ID to grant Infisical access to your Cloudflare Workers. - - ![integrations cloudflare authorization](../../images/integrations/cloudflare/integration-cloudflare-workers-connect.png) - - - - Select which Infisical environment secrets you want to sync to Cloudflare Workers and press create integration to start syncing secrets. - - ![integrations cloudflare](../../images/integrations/cloudflare/integration-cloudflare-workers-create.png) - - - diff --git a/docs/integrations/cloud/databricks.mdx b/docs/integrations/cloud/databricks.mdx deleted file mode 100644 index e5ad22939..000000000 --- a/docs/integrations/cloud/databricks.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Databricks" -description: "Learn how to sync secrets from Infisical to Databricks." ---- - - - The Databricks Native Integration will be deprecated in 2026. Please migrate to our new [Databricks Sync](../secret-syncs/databricks). - \ No newline at end of file diff --git a/docs/integrations/cloud/digital-ocean-app-platform.mdx b/docs/integrations/cloud/digital-ocean-app-platform.mdx deleted file mode 100644 index a0ed545cc..000000000 --- a/docs/integrations/cloud/digital-ocean-app-platform.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: "Digital Ocean App Platform" -description: "How to sync secrets from Infisical to Digital Ocean App Platform" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - -## Get your Digital Ocean Personal Access Tokens - -On Digital Ocean dashboard, navigate to **API > Tokens** and click on "Generate New Token" -![integrations digital ocean dashboard](../../images/integrations/digital-ocean/integrations-do-dashboard.png) - -Name it **infisical**, choose **No expiry**, and make sure to check **Write (optional)**. Then click on "Generate Token" and copy your API token. -![integrations digital ocean token modal](../../images/integrations/digital-ocean/integrations-do-token-modal.png) - -## Navigate to your project's integrations tab - -Click on the **Digital Ocean App Platform** tile and enter your API token to grant Infisical access to your Digital Ocean account. -![integrations](../../images/integrations.png) - -Then enter your Digital Ocean Personal Access Token here. Then click "Connect to Digital Ocean App Platform". -![integrations infisical dashboard digital ocean integration](../../images/integrations/digital-ocean/integrations-do-enter-token.png) - -## Start integration - -Select which Infisical environment secrets you want to sync to which Digital Ocean App and click "Create Integration". -![integrations digital ocean select projects](../../images/integrations/digital-ocean/integrations-do-select-projects.png) - -Done! -![integrations digital ocean integration success](../../images/integrations/digital-ocean/integrations-do-success.png) diff --git a/docs/integrations/cloud/flyio.mdx b/docs/integrations/cloud/flyio.mdx deleted file mode 100644 index 2aa14a919..000000000 --- a/docs/integrations/cloud/flyio.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: "Fly.io" -description: "How to sync secrets from Infisical to Fly.io" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Fly.io access token in Access Tokens - - ![integrations fly dashboard](../../images/integrations/flyio/integrations-flyio-dashboard.png) - ![integrations fly token](../../images/integrations/flyio/integrations-flyio-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Fly.io tile and input your Fly.io access token to grant Infisical access to your Fly.io account. - - ![integrations fly authorization](../../images/integrations/flyio/integrations-flyio-auth.png) - - - - Select which Infisical environment secrets you want to sync to which Fly.io app and press create integration to start syncing secrets to Fly.io. - - ![integrations fly](../../images/integrations/flyio/integrations-flyio-create.png) - ![integrations fly](../../images/integrations/flyio/integrations-flyio.png) - - \ No newline at end of file diff --git a/docs/integrations/cloud/gcp-secret-manager.mdx b/docs/integrations/cloud/gcp-secret-manager.mdx deleted file mode 100644 index 22462feef..000000000 --- a/docs/integrations/cloud/gcp-secret-manager.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "GCP Secret Manager" -description: "How to sync secrets from Infisical to GCP Secret Manager" ---- - - - The GCP Secret Manager Native Integration will be deprecated in 2026. Please migrate to our new [GCP Secret Manager Sync](../secret-syncs/gcp-secret-manager). - \ No newline at end of file diff --git a/docs/integrations/cloud/hashicorp-vault.mdx b/docs/integrations/cloud/hashicorp-vault.mdx deleted file mode 100644 index df2542ce7..000000000 --- a/docs/integrations/cloud/hashicorp-vault.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "HashiCorp Vault" -description: "How to sync secrets from Infisical to HashiCorp Vault" ---- - - - The Hashicorp Vault Native Integration will be deprecated in 2026. Please migrate to our new [Hashicorp Vault Sync](../secret-syncs/hashicorp-vault). - diff --git a/docs/integrations/cloud/hasura-cloud.mdx b/docs/integrations/cloud/hasura-cloud.mdx deleted file mode 100644 index f88c1eb50..000000000 --- a/docs/integrations/cloud/hasura-cloud.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Hasura Cloud" -description: "How to sync secrets from Infisical to Hasura Cloud" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Hasura Cloud Access Token in My Account > Access Tokens - - ![integrations hasura cloud tokens](../../images/integrations/hasura-cloud/integrations-hasura-cloud-tokens.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Hasura Cloud tile and input your Hasura Cloud access token to grant Infisical access to your Hasura Cloud account. - - ![integrations hasura cloud authorization](../../images/integrations/hasura-cloud/integrations-hasura-cloud-auth.png) - - - - Select which Infisical environment secrets you want to sync to which Hasura Cloud project and press create integration to start syncing secrets to Hasura Cloud. - - ![integrations hasura cloud](../../images/integrations/hasura-cloud/integrations-hasura-cloud-create.png) - ![integrations hasura cloud](../../images/integrations/hasura-cloud/integrations-hasura-cloud.png) - - \ No newline at end of file diff --git a/docs/integrations/cloud/heroku.mdx b/docs/integrations/cloud/heroku.mdx deleted file mode 100644 index 75cf8c106..000000000 --- a/docs/integrations/cloud/heroku.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: "Heroku" -description: "How to sync secrets from Infisical to Heroku" ---- - - - - Prerequisites: - - - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Heroku tile and grant Infisical access to your Heroku account. - - ![integrations heroku authorization](../../images/integrations/heroku/integrations-heroku-auth.png) - - - - Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku. - - ![integrations heroku](../../images/integrations/heroku/integrations-heroku-create.png) - - Here's some guidance on each field: - - - Project Environment: The environment in the current Infisical project from which you want to sync secrets from. - - Secrets Path: The path in the current Infisical project from which you want to sync secrets from such as `/` (for secrets that do not reside in a folder) or `/foo/bar` (for secrets nested in a folder, in this case a folder called `bar` in another folder called `foo`). - - Heroku App: The application in Heroku that you want to sync secrets to. - - Initial Sync Behavior (default is **Import - Prefer values from Infisical**): The behavior of the first sync operation triggered after creating the integration. - - **No Import - Overwrite all values in Heroku**: Sync secrets and overwrite any existing secrets in Heroku. - - **Import - Prefer values from Infisical**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, do nothing. Afterwards, sync secrets to Heroku. - - **Import - Prefer values from Heroku**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, replace its value with the one from Heroku. Afterwards, sync secrets to Heroku. - - ![integrations heroku](../../images/integrations/heroku/integrations-heroku.png) - - - - - Using the Heroku integration on a self-hosted instance of Infisical requires configuring an API client in Heroku - and registering your instance with it. - - - Navigate to your user Account settings > Applications to create a new API client. - - ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-settings.png) - ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-applications.png) - ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app.png) - - Create the API client. As part of the form, set the **OAuth callback URL** to `https://your-domain.com/integrations/heroku/oauth2/callback`. - - ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app-form.png) - - - Obtain the **Client ID** and **Client Secret** for your Heroku API client. - - ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-credentials.png) - - Back in your Infisical instance, add two new environment variables for the credentials of your Heroku API client. - - - `CLIENT_ID_HEROKU`: The **Client ID** of your Heroku API client. - - `CLIENT_SECRET_HEROKU`: The **Client Secret** of your Heroku API client. - - Once added, restart your Infisical instance and use the Heroku integration. - - - - diff --git a/docs/integrations/cloud/laravel-forge.mdx b/docs/integrations/cloud/laravel-forge.mdx deleted file mode 100644 index c58c4a7be..000000000 --- a/docs/integrations/cloud/laravel-forge.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Laravel Forge" -description: "How to sync secrets from Infisical to Laravel Forge" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Laravel Forge access token in API Tokens - - ![integrations laravel forge dashboard](../../images/integrations/laravel-forge/integrations-laravelforge-dashboard.png) - ![integrations laravel forge api tokens](../../images/integrations/laravel-forge/integrations-laravelforge-api.png) - - Obtain your Laravel Forge Server ID in Servers > Server ID - - ![integrations laravel forge server](../../images/integrations/laravel-forge/integrations-laravelforge-servers.png) - ![integrations laravel forge server id](../../images/integrations/laravel-forge/integrations-laravelforge-serverid.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Laravel Forge tile and input your Laravel Forge access token and server ID to grant Infisical access to your Laravel Forge account. - - ![integrations laravel forge authorization](../../images/integrations/laravel-forge/integrations-laravelforge-auth.png) - - - - Select which Infisical environment secrets you want to sync to which Laravel Forge site and press create integration to start syncing secrets to Laravel Forge. - - ![integrations laravel forge](../../images/integrations/laravel-forge/integrations-laravelforge-create.png) - ![integrations laravel forge](../../images/integrations/laravel-forge/integrations-laravelforge.png) - - - diff --git a/docs/integrations/cloud/netlify.mdx b/docs/integrations/cloud/netlify.mdx deleted file mode 100644 index f793aae10..000000000 --- a/docs/integrations/cloud/netlify.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: "Netlify" -description: "How to sync secrets from Infisical to Netlify" ---- - - - - - Infisical integrates with Netlify's new environment variable experience. If - your site uses Netlify's old environment variable experience, you'll have to - upgrade it to the new one to use this integration. - - - Prerequisites: - - - Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Netlify tile and grant Infisical access to your Netlify account. - - ![integrations netlify authorization](../../images/integrations/netlify/integrations-netlify-auth.png) - - - - Select which Infisical environment secrets you want to sync to which Netlify app and context. Lastly, press create integration to start syncing secrets to Netlify. - - ![integrations netlify](../../images/integrations/netlify/integrations-netlify-create.png) - ![integrations netlify](../../images/integrations/netlify/integrations-netlify.png) - - - - - Using the Netlify integration on a self-hosted instance of Infisical requires configuring an OAuth application in Netlify - and registering your instance with it. - - - Navigate to your User settings > Applications > OAuth to create a new OAuth application. - - ![integrations Netlify config](../../images/integrations/netlify/integrations-netlify-config-user-settings.png) - ![integrations Netlify config](../../images/integrations/netlify/integrations-netlify-config-new-app.png) - - Create the OAuth application. As part of the form, set the **Redirect URI** to `https://your-domain.com/integrations/netlify/oauth2/callback`. - - ![integrations Netlify config](../../images/integrations/netlify/integrations-netlify-config-new-app-form.png) - - - Obtain the **Client ID** and **Secret** for your Netlify OAuth application. - - ![integrations Netlify config](../../images/integrations/netlify/integrations-netlify-config-credentials.png) - - Back in your Infisical instance, add two new environment variables for the credentials of your Netlify OAuth application. - - - `CLIENT_ID_NETLIFY`: The **Client ID** of your Netlify OAuth application. - - `CLIENT_SECRET_NETLIFY`: The **Secret** of your Netlify OAuth application. - - Once added, restart your Infisical instance and use the Netlify integration. - - - - - diff --git a/docs/integrations/cloud/northflank.mdx b/docs/integrations/cloud/northflank.mdx deleted file mode 100644 index 10dcb288e..000000000 --- a/docs/integrations/cloud/northflank.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: "Northflank" -description: "How to sync secrets from Infisical to Northflank" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) -- Have a [Northflank](https://northflank.com) project with a secret group ready - - - - Obtain a Northflank API token in Account settings > API > Tokens - - ![integrations northflank dashboard](../../images/integrations/northflank/integrations-northflank-dashboard.png) - ![integrations northflank token](../../images/integrations/northflank/integrations-northflank-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Northflank tile and input your Northflank API token to grant Infisical access to your Northflank account. - - ![integrations northflank authorization](../../images/integrations/northflank/integrations-northflank-auth.png) - - - - Select which Infisical environment secrets you want to sync to which Northflank project and secret group. Finally, press create integration to start syncing secrets to Northflank. - - ![integrations northflank](../../images/integrations/northflank/integrations-northflank-create.png) - ![integrations northflank](../../images/integrations/northflank/integrations-northflank.png) - - \ No newline at end of file diff --git a/docs/integrations/cloud/qovery.mdx b/docs/integrations/cloud/qovery.mdx deleted file mode 100644 index 13aa6af46..000000000 --- a/docs/integrations/cloud/qovery.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Qovery" -description: "How to sync secrets from Infisical to Qovery" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Qovery API Token in Settings > API Token. - - ![integrations qovery api token](../../images/integrations/qovery/integrations-qovery-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Qovery tile and input your Qovery API Token to grant Infisical access to your Qovery account. - - ![integrations qovery authorization](../../images/integrations/qovery/integrations-qovery-auth.png) - - - - Select which Infisical environment secrets you want to sync to Qovery and press create integration to start syncing secrets. - - ![integrations qovery create](../../images/integrations/qovery/integrations-qovery-create-1.png) - - ![integrations qovery create](../../images/integrations/qovery/integrations-qovery-create-2.png) - - - Infisical supports syncing secrets to various Qovery scopes including applications, jobs, or containers. - - - ![integrations qovery settings](../../images/integrations/qovery/integrations-qovery.png) - - diff --git a/docs/integrations/cloud/railway.mdx b/docs/integrations/cloud/railway.mdx deleted file mode 100644 index 77b315517..000000000 --- a/docs/integrations/cloud/railway.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "Railway" -description: "How to sync secrets from Infisical to Railway" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Railway API Token in your Railway [Account Settings > Tokens](https://railway.app/account/tokens). - - ![integrations railway dashboard](../../images/integrations/railway/integrations-railway-dashboard.png) - ![integrations railway token](../../images/integrations/railway/integrations-railway-token.png) - - - If this is your first time creating a Railway API token, then you'll be prompted to join - Railway's Private Boarding Beta program on the Railway Account Settings > Tokens page. - - Note that Railway project tokens will not work for this integration since they don't work with - Railway's Public API. - - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Railway tile and input your Railway API Key to grant Infisical access to your Railway account. - - ![integrations railway authorization](../../images/integrations/railway/integrations-railway-authorization.png) - - - - Select which Infisical environment secrets you want to sync to which Railway project and environment (and optionally service). Lastly, press create integration to start syncing secrets to Railway. - - ![integrations create railway](../../images/integrations/railway/integrations-railway-create.png) - - - Infisical integrates with both Railway's [shared variables](https://blog.railway.app/p/shared-variables-release) at the project environment level as well as service variables at the service level. - - To sync secrets to a specific service in a project, you can select a service from the Railway Service dropdown; otherwise, leaving it empty will sync secrets to the shared variables of that project. - - - ![integrations railway](../../images/integrations/railway/integrations-railway.png) - - \ No newline at end of file diff --git a/docs/integrations/cloud/render.mdx b/docs/integrations/cloud/render.mdx deleted file mode 100644 index 1d4860ebd..000000000 --- a/docs/integrations/cloud/render.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: "Render" -description: "How to sync secrets from Infisical to Render" ---- - - - The Render Native Integration will be deprecated in 2026. Please migrate to - our new [Render Sync](../secret-syncs/render). - diff --git a/docs/integrations/cloud/supabase.mdx b/docs/integrations/cloud/supabase.mdx deleted file mode 100644 index b5179c45f..000000000 --- a/docs/integrations/cloud/supabase.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Supabase" -description: "How to sync secrets from Infisical to Supabase" ---- - - - The Supabase integration is useful if your Supabase project uses sensitive-information such as [environment variables in edge functions](https://supabase.com/docs/guides/functions/secrets). - - Synced envars can be accessed in edge functions using Deno's built-in handler: `Deno.env.get(MY_SECRET_NAME)`. - - -Prerequisites: - -- Have an account and project set up at [Supabase](https://supabase.com/) -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - - - - Obtain a Supabase Access Token in your Supabase [Account > Access Tokens](https://app.supabase.com/account/tokens). - ![integrations supabase dashboard](../../images/integrations/supabase/integrations-supabase-dashboard.png) - ![integrations supabase token](../../images/integrations/supabase/integrations-supabase-token.png) - - Navigate to your project's integrations tab in Infisical. - - ![integrations](../../images/integrations.png) - - Press on the Supabase tile and input your Supabase Access Token to grant Infisical access to your Supabase account. - - ![integrations supabase authorization](../../images/integrations/supabase/integrations-supabase-authorization.png) - - - - Select which Infisical environment secrets you want to sync to which Supabase project. Lastly, press create integration to start syncing secrets to Supabase. - - ![integrations supabase create](../../images/integrations/supabase/integrations-supabase-create.png) - - ![integrations supabase](../../images/integrations/supabase/integrations-supabase.png) - - diff --git a/docs/integrations/cloud/terraform-cloud.mdx b/docs/integrations/cloud/terraform-cloud.mdx deleted file mode 100644 index 63398ef4a..000000000 --- a/docs/integrations/cloud/terraform-cloud.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Terraform Cloud" -description: "How to sync secrets from Infisical to Terraform Cloud" ---- - - - The Terraform Cloud Native Integration will be deprecated in 2026. Please migrate to our new [Terraform Cloud Sync](../secret-syncs/terraform-cloud). - \ No newline at end of file diff --git a/docs/integrations/cloud/vercel.mdx b/docs/integrations/cloud/vercel.mdx deleted file mode 100644 index 7456776bd..000000000 --- a/docs/integrations/cloud/vercel.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Vercel" -description: "How to sync secrets from Infisical to Vercel" ---- - - - The Vercel Native Integration will be deprecated in 2026. Please migrate to our new [Vercel Sync](../secret-syncs/vercel). - \ No newline at end of file diff --git a/docs/integrations/cloud/windmill.mdx b/docs/integrations/cloud/windmill.mdx deleted file mode 100644 index 7d4c2cc82..000000000 --- a/docs/integrations/cloud/windmill.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "Windmill" -description: "How to sync secrets from Infisical to Windmill" ---- - - - The Windmill Native Integration will be deprecated in 2026. Please migrate to our new [Windmill Sync](../secret-syncs/windmill). - diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx deleted file mode 100644 index ed7d47b30..000000000 --- a/docs/integrations/overview.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: "Overview" -description: "How to use Infisical to inject secrets and configs into various 3-rd party services and frameworks." ---- - -Integrations allow environment variables to be synced from Infisical into your local development workflow, CI/CD pipelines, and production infrastructure. - -Missing an integration? [Throw in a request](https://github.com/Infisical/infisical/issues). - -| Integration | Type | Status | -| ------------------------------------------------------------------------------------- | ---------------------- | ---------------------------------- | -| [Docker](/integrations/platforms/docker) | Platform | Available | -| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available | -| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available | -| [Terraform](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) | Infrastructure as code | Available | -| [PM2](/integrations/platforms/pm2) | Platform | Available | -| [Heroku](/integrations/cloud/heroku) | Cloud | Available | -| [Vercel](/integrations/cloud/vercel) | Cloud | Available | -| [Netlify](/integrations/cloud/netlify) | Cloud | Available | -| [Render](/integrations/cloud/render) | Cloud | Available | -| [Laravel Forge](/integrations/cloud/laravel-forge) | Cloud | Available | -| [Railway](/integrations/cloud/railway) | Cloud | Available | -| [Terraform Cloud](/integrations/cloud/terraform-cloud) | Cloud | Available | -| [TeamCity](/integrations/cloud/teamcity) | Cloud | Available | -| [Fly.io](/integrations/cloud/flyio) | Cloud | Available | -| [Supabase](/integrations/cloud/supabase) | Cloud | Available | -| [Northflank](/integrations/cloud/northflank) | Cloud | Available | -| [Cloudflare Pages](/integrations/cloud/cloudflare-pages) | Cloud | Available | -| [Cloudflare Workers](/integrations/cloud/cloudflare-workers) | Cloud | Available | -| [Checkly](/integrations/cloud/checkly) | Cloud | Available | -| [Qovery](/integrations/cloud/qovery) | Cloud | Available | -| [HashiCorp Vault](/integrations/cloud/hashicorp-vault) | Cloud | Available | -| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | -| [AWS Secrets Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | -| [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | -| [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available | -| [Windmill](/integrations/cloud/windmill) | Cloud | Available | -| [Bitbucket](/integrations/cicd/bitbucket) | CI/CD | Available | -| [Codefresh](/integrations/cicd/codefresh) | CI/CD | Available | -| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available | -| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available | -| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available | -| [Travis CI](/integrations/cicd/travisci) | CI/CD | Available | -| [Rundeck](/integrations/cicd/rundeck) | CI/CD | Available | -| [Octopus Deploy](/integrations/cicd/octopus-deploy) | CI/CD | Available | -| [React](/integrations/frameworks/react) | Framework | Available | -| [Vue](/integrations/frameworks/vue) | Framework | Available | -| [Express](/integrations/frameworks/express) | Framework | Available | -| [Next.js](/integrations/frameworks/nextjs) | Framework | Available | -| [NestJS](/integrations/frameworks/nestjs) | Framework | Available | -| [SvelteKit](/integrations/frameworks/sveltekit) | Framework | Available | -| [Nuxt](/integrations/frameworks/nuxt) | Framework | Available | -| [Gatsby](/integrations/frameworks/gatsby) | Framework | Available | -| [Remix](/integrations/frameworks/remix) | Framework | Available | -| [Vite](/integrations/frameworks/vite) | Framework | Available | -| [Fiber](/integrations/frameworks/fiber) | Framework | Available | -| [Django](/integrations/frameworks/django) | Framework | Available | -| [Flask](/integrations/frameworks/flask) | Framework | Available | -| [Laravel](/integrations/frameworks/laravel) | Framework | Available | -| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available | -| Jenkins | CI/CD | Available | diff --git a/docs/integrations/platforms/ansible.mdx b/docs/integrations/platforms/ansible.mdx index 85f63079f..3eed68f05 100644 --- a/docs/integrations/platforms/ansible.mdx +++ b/docs/integrations/platforms/ansible.mdx @@ -36,7 +36,7 @@ You can either call modules by their Fully Qualified Collection Name (FQCN), suc ### Authentication -The Infisical Ansible Collection supports [Universal Auth](/documentation/platform/identities/universal-auth) and [OIDC](/documentation/platform/identities/oidc-auth/general) for authenticating against Infisical. +The Infisical Ansible Collection supports [Universal Auth](/documentation/platform/identities/universal-auth), [OIDC Auth](/documentation/platform/identities/oidc-auth/general), and [Token Auth](/documentation/platform/identities/token-auth) for authenticating against Infisical. @@ -77,6 +77,26 @@ The Infisical Ansible Collection supports [Universal Auth](/documentation/platfo | jwt | `INFISICAL_JWT` | + + + Token Auth is the simplest authentication method that allows you to authenticate directly with an access token. This can be either a [Machine Identity Token Auth](/documentation/platform/identities/token-auth) token or a User JWT token. + + + Please note that in order to use Token Auth, you must have `1.0.13` or newer of the `infisicalsdk` package installed. + + + ```yaml + lookup('infisical.vault.read_secrets', auth_method="token_auth", token='' ...rest) + ``` + + You can also provide the `auth_method` and `token` parameters through environment variables: + + | Parameter Name | Environment Variable Name | + | -------------- | ------------------------- | + | auth_method | `INFISICAL_AUTH_METHOD` | + | token | `INFISICAL_TOKEN` | + + ### Examples diff --git a/docs/integrations/platforms/aws/lambda.mdx b/docs/integrations/platforms/aws/lambda.mdx new file mode 100644 index 000000000..8376e98c2 --- /dev/null +++ b/docs/integrations/platforms/aws/lambda.mdx @@ -0,0 +1,98 @@ +--- +title: "AWS Lambda" +sidebarTitle: "AWS Lambda" +description: "How to use Infisical secrets in AWS Lambda" +--- + +Learn how to sync Infisical secrets to AWS Lambda regardless of how you deploy your function. This guide covers the following strategies: + +- Infisical SDKs +- AWS Secrets Manager integration +- AWS Systems Manager Parameter Store integration +- AWS CLI + +## Choose your sync strategy + +### 1. Fetch secrets at runtime with Infisical SDKs + +If you control the Lambda code, the simplest method is to fetch secrets directly from Infisical using one of our SDKs. +You can read more about the Infisical SDKs [here](/sdks/overview). + +### 2. Push via secret sync + +Configure a secret sync from your Infisical project, and Infisical will keep your Secrets Manager or Parameter Store values up to date. Your Lambda function can then reference those secrets directly. +Learn more about the [AWS Secrets Manager integration](/integrations/secret-syncs/aws-secrets-manager) and the [AWS Parameter Store integration](/integrations/secret-syncs/aws-parameter-store). + +### 3. Push environment variables directly using the AWS CLI + +For straightforward workflows or quick rotations, you can push Infisical secrets directly into Lambda environment variables using the AWS CLI. + +## Prerequisites + +- AWS CLI v2 installed and authenticated +- `jq` installed locally +- An IAM principal with `lambda:UpdateFunctionConfiguration` +- Infisical CLI (`infisical`) configured + +### IAM permissions + +Attach a policy like the one below to the IAM user or role responsible for updating Lambda configuration: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "LambdaConfig", + "Effect": "Allow", + "Action": ["lambda:UpdateFunctionConfiguration"], + "Resource": "*" + } + ] +} +``` + + + {" "} + Replacing Lambda environment variables using the AWS CLI overwrites the entire + `Variables` object. Make sure to export your current values so you can import them + into Infisical.{" "} + + +#### Push secrets to Lambda + +Use the Infisical CLI to export secrets as JSON and pass them to the AWS CLI. +The example below targets a project by ID, but you can also use the `--project` and `--env` flags. +Learn more about `infisical export` [here](/cli/commands/export#infisical-export). + +```bash +FUNCTION_NAME=infisical-env-test +REGION=us-east-1 +PROJECT_ID=1234567890 + +aws lambda update-function-configuration \ + --function-name "$FUNCTION_NAME" \ + --region "$REGION" \ + --environment "$( + infisical export \ + --format=json \ + --projectId="$PROJECT_ID" \ + | jq 'map({(.key): .value}) | add | {Variables: .}' + )" +``` + +On success, the updated `Environment.Variables` block will be returned. +Verify the values in the Lambda console or by invoking the function. + + + Automate this step in CI/CD. Run `infisical export` using an Infisical Token + scoped to your project and environment, and trigger the sync as part of your + deployment workflow. Learn more about the [Infisical + Token](/cli/commands/export#infisical-export:infisical-token). + + + + We recommend using automatic secret syncs to AWS Secrets Manager or AWS + Parameter Store to keep your secrets continuously in sync and avoid manually + updating the Lambda configuration. + diff --git a/docs/integrations/platforms/infisical-agent.mdx b/docs/integrations/platforms/infisical-agent.mdx index 43d322faa..883d248ae 100644 --- a/docs/integrations/platforms/infisical-agent.mdx +++ b/docs/integrations/platforms/infisical-agent.mdx @@ -8,12 +8,12 @@ It eliminates the need to modify application logic by enabling clients to decide ![agent diagram](/images/agent/infisical-agent-diagram.png) -### Key features: +## Key Features -- Token renewal: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume -- Templating: Renders secrets via user provided templates to desired formats for applications to consume +- **Token lifecycle management**: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume +- **Templating**: Renders secrets and dynamic secret leases via user provided templates to desired formats for applications to consume -### Token renewal +## Token Renewal The Infisical agent can help manage the life cycle of access tokens. The token renewal process is split into two main components: a `Method`, which is the authentication process suitable for your current setup, and `Sinks`, which are the places where the agent deposits the new access token whenever it receives updates. @@ -28,7 +28,7 @@ Every time the agent successfully retrieves a new access token, it writes the ne to retrieve secrets from Infisical -### Templating +## Templating The Infisical agent can help deliver formatted secrets to your application in a variety of environments. To achieve this, the agent will retrieve secrets from Infisical, format them using a specified template, and then save these formatted secrets to a designated file path. @@ -40,31 +40,203 @@ If this initial attempt is unsuccessful, the agent will momentarily pauses befor Once the agent successfully obtains a valid access token, the agent proceeds to fetch the secrets from Infisical using it. It then formats these secrets using the user provided templates and writes the formatted data to configured file paths. + +### Available secret template functions + +The secret template functions is what you will use to fetch resources such as static secrets and dynamic secret leases from Infisical. Below is a list of the available secret template functions that you can use in your templates. + + + + + ```bash + secret "" "environment-slug" "" "" + ``` + ```bash example-template-usage-1 + {{- with secret "6553ccb2b7da580d7f6e7260" "dev" "/" `{"recursive": false, "expandSecretReferences": true}` }} + {{- range . }} + {{ .Key }}={{ .Value }} + {{- end }} + {{- end }} + ``` + ```bash example-template-usage-2 + {{- with secret "da8056c8-01e2-4d24-b39f-cb4e004b8d44" "staging" "/" `{"recursive": true, "expandSecretReferences": true}` }} + {{- range . }} + {{- if eq .SecretPath "/"}} + {{ .Key }}={{ .Value }} + {{- else}} + {{ .SecretPath }}/{{ .Key }}={{ .Value }} + {{- end}} + {{- end }} + {{- end }} + ``` + + + + **Function name**: `secret` + + **Description**: This function can be used to render the full list of secrets within a given project, environment and secret path. + + An optional JSON argument is also available. It includes the properties `recursive`, which defaults to false, and `expandSecretReferences`, which defaults to true and expands the returned secrets. + + + **Returns**: A single secret object with the following keys `Key, WorkspaceId, Value, SecretPath, Type, ID, and Comment` + + + + + ```bash + getSecretByName "" "" "" "" + ``` + + ```bash example-template-usage + {{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}} + {{ if .Value }} + password = "{{ .Value }}" + {{ end }} + {{ end }} + ``` + + **Function name**: `getSecretByName` + + **Description**: This function can be used to render a single secret by it's name. + + **Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment` + + + + + ```bash + dynamic_secret "" "" "" "" "" + ``` + + ```bash example-redis-dynamic-secret + {{ with dynamic_secret "aaa-o7en-s5qm" "dev" "/" "redis" "1m" }} + {{ .DB_USERNAME }}={{ .DB_PASSWORD }} + {{- end }} + + ``` + + **Function Name**: `dynamic_secret` + + **Description**: This function can be used to render a dynamic secret lease credentials. The credentials are automatically renewed before they expire, ensuring that the rendered credentials are always up-to-date. + + **Returns**: An object with keys corresponding to the dynamic secret lease credentials. + + + Note that if you have multiple dynamic secret templates with identical configurations, only one lease will be created in Infisical for those templates, and the same lease will be written to your specified destination paths. + + + + + +## Caching + +The Infisical Agent supports clientside caching of Dynamic Secret leases. If the cache is enabled, the agent will persist the dynamic secret leases to the cache across restarts of the agent. + +### Persistent Caching + +The Agent currently only supports persistent caching. To utilize persistent caching, you must be within a Kubernetes environment. We recommend using the [Infisical Agent Injector](/integrations/platforms/kubernetes-injector) to inject the agent into pods within your Kubernetes cluster on demand. + +### Cache eviction + +Cache eviction is the process of removing cached data from the cache. The Agent will automatically evict cached data when the cache is full during a garbage collection cycle which is triggered every 10 minutes. + +The cache will also automatically evict cached data that has gone stale or is about to go stale. For dynamic resources (such as dynamic secret leases), there's a TTL (Time-to-Live) associated with each lease which is used to determine if the lease is stale or about to go stale. +If a stale dynamic secret lease is detected, it will be automatically evicted from the cache and replaced with a new up-to-date lease. + + +### Cache Configuration + +Configuring the cache is done through the agent configuration file. The following fields are available to configure the cache: + + + + + The type of persistent caching to use. Currently only `kubernetes` is available, and will only work within Kubernetes environments. + + + The path to where your persistent cache will be stored. + + + + Persistent caching is only supported within kubernetes environments at the moment. Please refer to the [Infisical Agent Injector](/integrations/platforms/kubernetes-injector) documentation for more information on how to use persistent caching within Kubernetes environments. + + + ```yaml example-agent-config-file.yaml + cache: + persistent: + type: "kubernetes" + path: "/home/infisical/cache" + service-account-token-path: "/var/run/secrets/kubernetes.io/serviceaccount/token" + ``` + + + + +## Retrying mechanism + +The agent will automatically attempt to retry failed API requests such as authentication, secrets retrieval, dynamic secret lease provisioning, etc. +By default, the agent will retry up to 3 times with a base delay of 200ms and a maximum delay of 5s. + +You can configure the retrying mechanism through the agent configuration file. The following fields are available to configure the retrying mechanism: + + + + How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. + + + The maximum delay between retries. Defaults to `5s` (5 seconds). + + + The base delay between retries. Defaults to `200ms` (200 milliseconds). + + +```yaml example-agent-config-file.yaml +infisical: + address: "https://app.infisical.com" + retry-strategy: + max-retries: 3 + max-delay: "5s" + base-delay: "200ms" + +# ... rest of the agent configuration file +``` + + + ## Agent configuration file To set up the authentication method for token renewal and to define secret templates, the Infisical agent requires a YAML configuration file containing properties defined below. While specifying an authentication method is mandatory to start the agent, configuring sinks and secret templates are optional. -| Field | Description | -| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. | -| `infisical.exit-after-auth` | Whether to exit the agent after authentication and first secret render. Default: `"false"`. | -| `infisical.revoke-credentials-on-shutdown` | Whether to revoke all managed dynamic secret leases and identity access tokens on shutdown. Default: `"false"`. | -| `auth.type` | The type of authentication method used. Available options: `universal-auth`, `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, `aws-iam` | -| `auth.config.identity-id` | The file path where the machine identity id is stored

This field is required when using any of the following auth types: `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. | -| `auth.config.service-account-token` | Path to the Kubernetes service account token to use (optional)

Default: `/var/run/secrets/kubernetes.io/serviceaccount/token` | -| `auth.config.service-account-key` | Path to your GCP service account key file. This field is required when using `gcp-iam` auth type.

Please note that the file should be in JSON format. | -| `auth.config.client-id` | The file path where the universal-auth client id is stored. | -| `auth.config.client-secret` | The file path where the universal-auth client secret is stored. | -| `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. | -| `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. | -| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | -| `templates[].source-path` | The path to the template file that should be used to render secrets. | -| `templates[].template-content` | The inline secret template to be used for rendering the secrets. | -| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. | -| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5 minutes` (optional) | -| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) | -| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) | + + +| Field | Description | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. | +| `infisical.exit-after-auth` | Whether to exit the agent after authentication and first secret render. Default: `"false"`. | +| `infisical.revoke-credentials-on-shutdown` | Whether to revoke all managed dynamic secret leases and identity access tokens on shutdown. Default: `"false"`. | +| `infisical.retry-strategy.max-retries` | How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. | +| `infisical.retry-strategy.max-delay` | The maximum delay between retries. Defaults to `5s` (5 seconds). | +| `infisical.retry-strategy.base-delay` | The base delay between retries. Defaults to `200ms` (200 milliseconds). | +| `auth.type` | The type of authentication method used. Available options: `universal-auth`, `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, `aws-iam` | +| `auth.config.identity-id` | The file path where the machine identity id is stored

This field is required when using any of the following auth types: `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. | +| `auth.config.service-account-token` | Path to the Kubernetes service account token to use (optional)

Default: `/var/run/secrets/kubernetes.io/serviceaccount/token` | +| `auth.config.service-account-key` | Path to your GCP service account key file. This field is required when using `gcp-iam` auth type.

Please note that the file should be in JSON format. | +| `auth.config.client-id` | The file path where the universal-auth client id is stored. | +| `auth.config.client-secret` | The file path where the universal-auth client secret is stored. | +| `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. | +| `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. | +| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | +| `cache.persistent.type` | The type of persistent caching to use. Currently only `kubernetes` is available, and will only work within Kubernetes environments. | +| `cache.persistent.path` | The path to where your persistent cache will be stored. | +| `cache.persistent.service-account-token-path` | The path to the Kubernetes service account token to use for encrypting the persistent cache. Required when using `kubernetes` cache type. Defaults to `/var/run/secrets/kubernetes.io/serviceaccount/token` | +| `templates[].source-path` | The path to the template file that should be used to render secrets. | +| `templates[].template-content` | The inline secret template to be used for rendering the secrets. | +| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. | +| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5m` (5 minutes) (optional) | +| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) | +| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) | ## Authentication @@ -308,81 +480,4 @@ After defining the agent configuration file, run the command below pointing to t ```bash infisical agent --config example-agent-config-file.yaml -``` - -### Available secret template functions - - - ```bash - listSecrets "" "environment-slug" "" "" - ``` - ```bash example-template-usage-1 - {{- with listSecrets "6553ccb2b7da580d7f6e7260" "dev" "/" `{"recursive": false, "expandSecretReferences": true}` }} - {{- range . }} - {{ .Key }}={{ .Value }} - {{- end }} - {{- end }} - ``` - ```bash example-template-usage-2 -{{- with secret "da8056c8-01e2-4d24-b39f-cb4e004b8d44" "staging" "/" `{"recursive": true, "expandSecretReferences": true}` }} -{{- range . }} -{{- if eq .SecretPath "/"}} -{{ .Key }}={{ .Value }} -{{- else}} -{{ .SecretPath }}/{{ .Key }}={{ .Value }} -{{- end}} -{{- end }} -{{- end }} - ``` - - - -**Function name**: listSecrets - -**Description**: This function can be used to render the full list of secrets within a given project, environment and secret path. - -An optional JSON argument is also available. It includes the properties `recursive`, which defaults to false, and `expandSecretReferences`, which defaults to true and expands the returned secrets. - - -**Returns**: A single secret object with the following keys `Key, WorkspaceId, Value, SecretPath, Type, ID, and Comment` - - - - - ```bash - getSecretByName "" "" "" "" - ``` - -```bash example-template-usage -{{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}} -{{ if .Value }} -password = "{{ .Value }}" -{{ end }} -{{ end }} -``` - -**Function name**: getSecretByName - -**Description**: This function can be used to render a single secret by it's name. - -**Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment` - - - - - ```bash - dynamic_secret "" "" "" "" "" - ``` - - ```bash example-redis-dynamic-secret - {{ with dynamic_secret "aaa-o7en-s5qm" "dev" "/" "redis" "1m" }} - {{ .DB_USERNAME }}={{ .DB_PASSWORD }} - {{- end }} - - **Function Name**: dynamic_secret - - **Description**: This function can be used to render a dynamic secret lease credentials. The credentials are automatically renewed before they expire, ensuring that the rendered credentials are always up-to-date. - - **Returns**: An object with keys corresponding to the dynamic secret lease credentials. - ``` - \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/integrations/platforms/kubernetes-injector.mdx b/docs/integrations/platforms/kubernetes-injector.mdx index 9903dcbc3..f51a96ab2 100644 --- a/docs/integrations/platforms/kubernetes-injector.mdx +++ b/docs/integrations/platforms/kubernetes-injector.mdx @@ -120,19 +120,83 @@ You will need to set the `nodeSelector.kubernetes.io/os` label to `windows` and The Infisical Agent Injector supports the following annotations: - - The inject annotation is used to enable the injector on a pod. Set the value to `true` and the pod will be patched with an Infisical Agent container on update or create. - - - The inject mode annotation is used to specify the mode to use to inject the secrets into the pod. + + + The inject annotation is used to enable the injector on a pod. Set the value to `true` and the pod will be patched with an Infisical Agent container on update or create. + + + The inject mode annotation is used to specify the mode to use to inject the secrets into the pod. - - `init`: The init method will create an init container for the pod that will render the secrets into a shared volume mount within the pod. The agent init container will run before any other containers in the pod runs, including other init containers. - - `sidecar`: The sidecar method will create a sidecar container for the pod that will render the secrets into a shared volume mount within the pod. The agent sidecar container will run alongside the main container in the pod. This means that the secrets rendered will always be in sync with your Infisical secrets. - - `sidecar-init`: The sidecar-init method will create the init container and the sidecar container from the other two methods. The init container will run before any other container and fetch the secrets from the start and the sidecar container will keep the secrets in sync throughout the lifecycle of the deployment. - - - The agent config map annotation is used to specify the name of the config map that contains the configuration for the injector. The config map must be in the same namespace as the pod. - + - `init`: The init method will create an init container for the pod that will render the secrets into a shared volume mount within the pod. The agent init container will run before any other containers in the pod runs, including other init containers. + - `sidecar`: The sidecar method will create a sidecar container for the pod that will render the secrets into a shared volume mount within the pod. The agent sidecar container will run alongside the main container in the pod. This means that the secrets rendered will always be in sync with your Infisical secrets. + - `sidecar-init`: The sidecar-init method will create the init container and the sidecar container from the other two methods. The init container will run before any other container and fetch the secrets from the start and the sidecar container will keep the secrets in sync throughout the lifecycle of the deployment. + + + The agent config map annotation is used to specify the name of the config map that contains the configuration for the injector. The config map must be in the same namespace as the pod. + + + + Whether to enable client-side caching of dynamic secret leases. Defaults to `false`. If you set this to `true`, the agent will persist any dynamic secret leases across restarts of the agent. This is especially useful when using the `sidecar-init` inject mode, to pass the dynamic secret leases created in the init container to the sidecar container. + This will ensure that no new leases are created except those initially created in the init container. The sidecar container will register the leases created in the init container and start managing them from that point onwards. + + + + Whether to revoke all managed dynamic secret leases and machine identity access tokens on shutdown. Defaults to `false`. + + If you set this to `true`, all managed dynamic secret leases and machine identity access tokens will be revoked when a `SIGTERM` signal is sent to the agents container _(such as when a pod is terminated or when the pod is restarted)_. + + **Note:** In disaster events such as cluster power outages, a `SIGTERM` signal won't be sent to the agents container, and the credentials will not be revoked. + + + + How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + + The maximum delay between retries. Defaults to `5s` (5 seconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + + The base delay between retries. Defaults to `200ms` (200 milliseconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + + The maximum CPU limit for the agent containers. + + Linux Pods: Defaults to `500m` (500 milliCPUs). + Windows Pods: Defaults to `500m` (500 milliCPUs). + + + + The minimum CPU request for the agent containers. + + Linux Pods: Defaults to `100m` (100 milliCPUs). + Windows Pods: Defaults to `100m` (100 milliCPUs). + + + + The maximum memory limit for the agent containers. + + Linux Pods: Defaults to `128Mi` (128 megabytes). + Windows Pods: Defaults to `512Mi` (512 megabytes). + + + + The minimum memory request for the agent containers. + + Linux Pods: Defaults to `64Mi` (64 megabytes). + Windows Pods: Defaults to `256Mi` (256 megabytes). + + + + The maximum ephemeral storage limit for the agent containers. Doesn't have an explicit default value. The default value will conform to the default ephemeral storage limit for the pod. + + + + The minimum ephemeral storage request for the agent containers. Doesn't have an explicit default value. The default value will conform to the default ephemeral storage request for the pod. + + + ## ConfigMap Configuration @@ -141,18 +205,22 @@ The Infisical Agent Injector supports the following annotations: When you are configuring a pod to use the injector, you must create a config map in the same namespace as the pod you want to inject secrets into. The entire config needs to be of string format and needs to be assigned to the `config.yaml` key in the config map. You can find a full example of the config at the end of this section. + The address of your Infisical instance. This field is optional and will default to `https://app.infisical.com` if not provided. - Whether to revoke all managed dynamic secret leases and identity access tokens on shutdown. Default: `"false"`. + Whether to revoke all managed dynamic secret leases and machine identity access tokens on shutdown. Default: `"false"`. - If this is set to `true`, all managed dynamic secret leases and identity access tokens will be revoked when a `SIGTERM` signal is sent to the agents container _(such as when a pod is terminated or when the pod is restarted)_. + If this is set to `true`, all managed dynamic secret leases and machine identity access tokens will be revoked when a `SIGTERM` signal is sent to the agents container _(such as when a pod is terminated or when the pod is restarted)_. + **Note:** In disaster events such as cluster power outages, a `SIGTERM` signal won't be sent to the agents container, and the credentials will not be revoked. - Note that this is currently unsupported on Windows-based pods, and will only work when injecting into Linux-based pods. + This is currently unsupported on Windows-based pods, and will only work when injecting into Linux-based pods. + + It's recommended to use the annotation `org.infisical.com/agent-revoke-on-shutdown: "true"` instead of configuring the revoke on shutdown on the config map. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the revoke on shutdown through annotations. @@ -162,8 +230,59 @@ The entire config needs to be of string format and needs to be assigned to the ` Please note that the pod's default service account will be used to authenticate with Infisical. + - The ID of the machine identity to use to connect to Infisical. This field is required if the `infisical.auth.type` is set to `kubernetes`. + The ID of the machine identity to use for Kubernetes or LDAP authentication. This field is required if the `infisical.auth.type` is set to `kubernetes`. + + + + The LDAP username to use for LDAP authentication. + This field is required if the `infisical.auth.type` is set to `ldap-auth`. + + + + The LDAP password to use for LDAP authentication. + This field is required if the `infisical.auth.type` is set to `ldap-auth`. + + + + How many times to retry failed API requests such as authentication, secret retrieval, etc. Defaults to `3` retries. Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + You can also configure the max retries through annotations. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the max retries through annotations. + + + + + The maximum delay between retries. Defaults to `5s` (5 seconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + You can also configure the max delay through annotations. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the max delay through annotations. + + + + + The base delay between retries. Defaults to `200ms` (200 milliseconds). Refer to the [Retrying mechanism](/integrations/platforms/infisical-agent#retrying-mechanism) documentation for more information on how to configure the retry strategy. + + + You can also configure the base delay through annotations. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the base delay through annotations. + + + + + The type of persistent caching to use. Currently only `kubernetes` is available, and will only work within Kubernetes environments. + + + It is recommended to use the annotation `org.infisical.com/agent-cache-enabled: "true"` instead of configuring the cache on the config map. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the cache through annotations. + + + + + The path to the Kubernetes service account token to use for encrypting the persistent cache. Required when using `kubernetes` cache type. Defaults to `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + + It is recommended to use the annotation `org.infisical.com/agent-cache-enabled: "true"` instead of configuring the cache on the config map. Refer to the [Supported annotations](/integrations/platforms/kubernetes-injector#supported-annotations) documentation for more information on how to configure the cache through annotations. + @@ -180,6 +299,7 @@ The templates hold an array of templates that will be rendered and injected into This will be rendered as a [Go Template](https://pkg.go.dev/text/template) and will have access to the following variables. It follows the templating format and supports the same functions as the [Infisical Agent](/integrations/platforms/infisical-agent#quick-start-infisical-agent) + ### Authentication @@ -271,7 +391,7 @@ The Infisical Agent Injector supports Machine Identity [Kubernetes Auth](/docume -To use the config map in your pod, you will need to add the `org.infisical.com/agent-config-map` annotation to your pod's deployment. The value of the annotation is the name of the config map you created above. +To use the config map in your pod, you will need to add the `org.infisical.com/agent-config-map` annotation to your pod's deployment. The value of the annotation is the name of the config map you created above. The config map must be in the same namespace as the pod you're injecting into. ```yaml apiVersion: v1 kind: Pod diff --git a/docs/integrations/secret-syncs/overview.mdx b/docs/integrations/secret-syncs/overview.mdx index 937c8d826..c341e6ea3 100644 --- a/docs/integrations/secret-syncs/overview.mdx +++ b/docs/integrations/secret-syncs/overview.mdx @@ -5,10 +5,6 @@ description: "Learn how to sync secrets to third-party services with Infisical." Secret Syncs enable you to sync secrets from Infisical to third-party services using [App Connections](/integrations/app-connections/overview). - - Secret Syncs will gradually replace Native Integrations as they become available. Native Integrations will be deprecated in the future, so opt for configuring a Secret Sync when available. - - ## Concept Secret Syncs are a project-level resource used to sync secrets, via an [App Connection](/integrations/app-connections/overview), from a particular project environment and folder path (source) @@ -92,7 +88,7 @@ via the UI or API for the third-party service you intend to sync secrets to. Infisical is continuously expanding it's Secret Sync third-party service support. If the service you need isn't available, - you can still use our Native Integrations in the interim, or contact us at team@infisical.com to make a request . + you can contact us at team@infisical.com to make a request. ## Key Schemas diff --git a/docs/sdks/languages/python.mdx b/docs/sdks/languages/python.mdx index 670e0975a..d69f19173 100644 --- a/docs/sdks/languages/python.mdx +++ b/docs/sdks/languages/python.mdx @@ -108,6 +108,15 @@ response = client.auth.oidc_auth.login( This authentication method is useful when integrating with OIDC-compliant identity providers like Okta, Auth0, or any service that issues OIDC tokens. +#### Token Auth + +```python +response = client.auth.token_auth.login(token="") +``` + +**Parameters:** +- `token` (str): The access token to authenticate with. This can be a [machine identity token](/documentation/platform/identities/token-auth) or a user access token. + ### `secrets` This sub-class handles operations related to secrets: diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index edebf1670..669c3aaa3 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -703,110 +703,6 @@ You can configure third-party app connections for re-use across Infisical Projec -## Native Secret Integrations - -To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box. - - - - OAuth2 client ID for Heroku integration - - - OAuth2 client secret for Heroku integration - - - - - - OAuth2 client ID for Vercel integration - - -{" "} - - - OAuth2 client secret for Vercel integration - - - - OAuth2 slug for Vercel integration - - - - - - OAuth2 client ID for Netlify integration - - - - OAuth2 client secret for Netlify integration - - - - - - OAuth2 client ID for GitHub integration - - - - OAuth2 client secret for GitHub integration - - - - - - OAuth2 client ID for Bitbucket integration - - - - OAuth2 client secret for Bitbucket integration - - - - - - OAuth2 client id for GCP secrets manager integration - - - - OAuth2 client secret for GCP secrets manager integration - - - - - - The AWS IAM User access key for assuming roles. - - - - The AWS IAM User secret key for assuming roles. - - - - - - OAuth2 client id for Azure integration - - - - OAuth2 client secret for Azure integration - - - - - - OAuth2 client id for Gitlab integration - - - - OAuth2 client secret for Gitlab integration - - - ## Secret Scanning diff --git a/docs/snippets/AppConnectionsBrowser.jsx b/docs/snippets/AppConnectionsBrowser.jsx index dfe574547..d7001d6eb 100644 --- a/docs/snippets/AppConnectionsBrowser.jsx +++ b/docs/snippets/AppConnectionsBrowser.jsx @@ -1,70 +1,388 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo } from "react"; export const AppConnectionsBrowser = () => { - const [searchTerm, setSearchTerm] = useState(''); - const [selectedCategory, setSelectedCategory] = useState('All'); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedCategory, setSelectedCategory] = useState("All"); - const categories = ['All', 'Cloud Providers', 'Databases', 'CI/CD', 'Monitoring', 'Directory Services', 'Identity & Auth', 'Data Analytics', 'Hosting', 'DevOps Tools', 'Security']; + const categories = [ + "All", + "Cloud Providers", + "Databases", + "CI/CD", + "Monitoring", + "Directory Services", + "Identity & Auth", + "Data Analytics", + "Hosting", + "DevOps Tools", + "Security", + "Networking & DNS", + ]; const connections = [ - {"name": "AWS", "slug": "aws", "path": "/integrations/app-connections/aws", "description": "Learn how to connect your AWS applications to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Azure Key Vault", "slug": "azure-key-vault", "path": "/integrations/app-connections/azure-key-vault", "description": "Learn how to connect your Azure Key Vault to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Azure App Configuration", "slug": "azure-app-configuration", "path": "/integrations/app-connections/azure-app-configuration", "description": "Learn how to connect your Azure App Configuration to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Azure Client Secrets", "slug": "azure-client-secrets", "path": "/integrations/app-connections/azure-client-secrets", "description": "Learn how to connect your Azure Client Secrets to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Azure DevOps", "slug": "azure-devops", "path": "/integrations/app-connections/azure-devops", "description": "Learn how to connect your Azure DevOps to pull secrets from Infisical.", "category": "CI/CD"}, - {"name": "Azure ADCS", "slug": "azure-adcs", "path": "/integrations/app-connections/azure-adcs", "description": "Learn how to connect your Azure ADCS to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "GCP", "slug": "gcp", "path": "/integrations/app-connections/gcp", "description": "Learn how to connect your GCP applications to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "HashiCorp Vault", "slug": "hashicorp-vault", "path": "/integrations/app-connections/hashicorp-vault", "description": "Learn how to connect your HashiCorp Vault to pull secrets from Infisical.", "category": "Security"}, - {"name": "1Password", "slug": "1password", "path": "/integrations/app-connections/1password", "description": "Learn how to connect your 1Password to pull secrets from Infisical.", "category": "Security"}, - {"name": "Vercel", "slug": "vercel", "path": "/integrations/app-connections/vercel", "description": "Learn how to connect your Vercel application to pull secrets from Infisical.", "category": "Hosting"}, - {"name": "Netlify", "slug": "netlify", "path": "/integrations/app-connections/netlify", "description": "Learn how to connect your Netlify application to pull secrets from Infisical.", "category": "Hosting"}, - {"name": "Railway", "slug": "railway", "path": "/integrations/app-connections/railway", "description": "Learn how to connect your Railway application to pull secrets from Infisical.", "category": "Hosting"}, - {"name": "Fly.io", "slug": "flyio", "path": "/integrations/app-connections/flyio", "description": "Learn how to connect your Fly.io application to pull secrets from Infisical.", "category": "Hosting"}, - {"name": "Render", "slug": "render", "path": "/integrations/app-connections/render", "description": "Learn how to connect your Render application to pull secrets from Infisical.", "category": "Hosting"}, - {"name": "Heroku", "slug": "heroku", "path": "/integrations/app-connections/heroku", "description": "Learn how to connect your Heroku application to pull secrets from Infisical.", "category": "Hosting"}, - {"name": "DigitalOcean", "slug": "digital-ocean", "path": "/integrations/app-connections/digital-ocean", "description": "Learn how to connect your DigitalOcean application to pull secrets from Infisical.", "category": "Hosting"}, - {"name": "Supabase", "slug": "supabase", "path": "/integrations/app-connections/supabase", "description": "Learn how to connect your Supabase application to pull secrets from Infisical.", "category": "Databases"}, - {"name": "Checkly", "slug": "checkly", "path": "/integrations/app-connections/checkly", "description": "Learn how to connect your Checkly application to pull secrets from Infisical.", "category": "Monitoring"}, - {"name": "GitHub", "slug": "github", "path": "/integrations/app-connections/github", "description": "Learn how to connect your GitHub application to pull secrets from Infisical.", "category": "CI/CD"}, - {"name": "GitHub Radar", "slug": "github-radar", "path": "/integrations/app-connections/github-radar", "description": "Learn how to connect your GitHub Radar to pull secrets from Infisical.", "category": "CI/CD"}, - {"name": "GitLab", "slug": "gitlab", "path": "/integrations/app-connections/gitlab", "description": "Learn how to connect your GitLab application to pull secrets from Infisical.", "category": "CI/CD"}, - {"name": "TeamCity", "slug": "teamcity", "path": "/integrations/app-connections/teamcity", "description": "Learn how to connect your TeamCity to pull secrets from Infisical.", "category": "CI/CD"}, - {"name": "Bitbucket", "slug": "bitbucket", "path": "/integrations/app-connections/bitbucket", "description": "Learn how to connect your Bitbucket to pull secrets from Infisical.", "category": "CI/CD"}, - {"name": "Terraform Cloud", "slug": "terraform-cloud", "path": "/integrations/app-connections/terraform-cloud", "description": "Learn how to connect your Terraform Cloud to pull secrets from Infisical.", "category": "DevOps Tools"}, - {"name": "Cloudflare", "slug": "cloudflare", "path": "/integrations/app-connections/cloudflare", "description": "Learn how to connect your Cloudflare application to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Databricks", "slug": "databricks", "path": "/integrations/app-connections/databricks", "description": "Learn how to connect your Databricks to pull secrets from Infisical.", "category": "Data Analytics"}, - {"name": "Windmill", "slug": "windmill", "path": "/integrations/app-connections/windmill", "description": "Learn how to connect your Windmill to pull secrets from Infisical.", "category": "DevOps Tools"}, - {"name": "Camunda", "slug": "camunda", "path": "/integrations/app-connections/camunda", "description": "Learn how to connect your Camunda to pull secrets from Infisical.", "category": "DevOps Tools"}, - {"name": "Humanitec", "slug": "humanitec", "path": "/integrations/app-connections/humanitec", "description": "Learn how to connect your Humanitec to pull secrets from Infisical.", "category": "DevOps Tools"}, - {"name": "OCI", "slug": "oci", "path": "/integrations/app-connections/oci", "description": "Learn how to connect your OCI applications to pull secrets from Infisical.", "category": "Cloud Providers"}, - {"name": "Zabbix", "slug": "zabbix", "path": "/integrations/app-connections/zabbix", "description": "Learn how to connect your Zabbix to pull secrets from Infisical.", "category": "Monitoring"}, - {"name": "MySQL", "slug": "mysql", "path": "/integrations/app-connections/mysql", "description": "Learn how to connect your MySQL database to pull secrets from Infisical.", "category": "Databases"}, - {"name": "PostgreSQL", "slug": "postgres", "path": "/integrations/app-connections/postgres", "description": "Learn how to connect your PostgreSQL database to pull secrets from Infisical.", "category": "Databases"}, - {"name": "Microsoft SQL Server", "slug": "mssql", "path": "/integrations/app-connections/mssql", "description": "Learn how to connect your SQL Server database to pull secrets from Infisical.", "category": "Databases"}, - {"name": "Oracle Database", "slug": "oracledb", "path": "/integrations/app-connections/oracledb", "description": "Learn how to connect your Oracle database to pull secrets from Infisical.", "category": "Databases"}, - {"name": "Redis", "slug": "redis", "path": "/integrations/app-connections/redis", "description": "Learn how to connect Redis to pull secrets from Infisical.", "category": "Databases"}, - {"name": "LDAP", "slug": "ldap", "path": "/integrations/app-connections/ldap", "description": "Learn how to connect your LDAP to pull secrets from Infisical.", "category": "Directory Services"}, - {"name": "Auth0", "slug": "auth0", "path": "/integrations/app-connections/auth0", "description": "Learn how to connect your Auth0 to pull secrets from Infisical.", "category": "Identity & Auth"}, - {"name": "Okta", "slug": "okta", "path": "/integrations/app-connections/okta", "description": "Learn how to connect your Okta to pull secrets from Infisical.", "category": "Identity & Auth"}, - {"name": "Laravel Forge", "slug": "laravel-forge", "path": "/integrations/app-connections/laravel-forge", "description": "Learn how to connect your Laravel Forge to pull secrets from Infisical.", "category": "Hosting"}, - {"name": "Chef", "slug": "chef", "path": "/integrations/app-connections/chef", "description": "Learn how to connect your Chef to pull secrets from Infisical.", "category": "DevOps Tools"}, - {"name": "Northflank", "slug": "northflank", "path": "/integrations/app-connections/northflank", "description": "Learn how to connect your Northflank projects to pull secrets from Infisical.", "category": "Hosting"} - ].sort(function(a, b) { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + { + name: "AWS", + slug: "aws", + path: "/integrations/app-connections/aws", + description: + "Learn how to connect your AWS applications to pull secrets from Infisical.", + category: "Cloud Providers", + }, + { + name: "Azure Key Vault", + slug: "azure-key-vault", + path: "/integrations/app-connections/azure-key-vault", + description: + "Learn how to connect your Azure Key Vault to pull secrets from Infisical.", + category: "Cloud Providers", + }, + { + name: "Azure App Configuration", + slug: "azure-app-configuration", + path: "/integrations/app-connections/azure-app-configuration", + description: + "Learn how to connect your Azure App Configuration to pull secrets from Infisical.", + category: "Cloud Providers", + }, + { + name: "Azure Client Secrets", + slug: "azure-client-secrets", + path: "/integrations/app-connections/azure-client-secrets", + description: + "Learn how to connect your Azure Client Secrets to pull secrets from Infisical.", + category: "Cloud Providers", + }, + { + name: "Azure DevOps", + slug: "azure-devops", + path: "/integrations/app-connections/azure-devops", + description: + "Learn how to connect your Azure DevOps to pull secrets from Infisical.", + category: "CI/CD", + }, + { + name: "Azure ADCS", + slug: "azure-adcs", + path: "/integrations/app-connections/azure-adcs", + description: + "Learn how to connect your Azure ADCS to pull secrets from Infisical.", + category: "Cloud Providers", + }, + { + name: "GCP", + slug: "gcp", + path: "/integrations/app-connections/gcp", + description: + "Learn how to connect your GCP applications to pull secrets from Infisical.", + category: "Cloud Providers", + }, + { + name: "HashiCorp Vault", + slug: "hashicorp-vault", + path: "/integrations/app-connections/hashicorp-vault", + description: + "Learn how to connect your HashiCorp Vault to pull secrets from Infisical.", + category: "Security", + }, + { + name: "1Password", + slug: "1password", + path: "/integrations/app-connections/1password", + description: + "Learn how to connect your 1Password to pull secrets from Infisical.", + category: "Security", + }, + { + name: "Vercel", + slug: "vercel", + path: "/integrations/app-connections/vercel", + description: + "Learn how to connect your Vercel application to pull secrets from Infisical.", + category: "Hosting", + }, + { + name: "Netlify", + slug: "netlify", + path: "/integrations/app-connections/netlify", + description: + "Learn how to connect your Netlify application to pull secrets from Infisical.", + category: "Hosting", + }, + { + name: "Railway", + slug: "railway", + path: "/integrations/app-connections/railway", + description: + "Learn how to connect your Railway application to pull secrets from Infisical.", + category: "Hosting", + }, + { + name: "Fly.io", + slug: "flyio", + path: "/integrations/app-connections/flyio", + description: + "Learn how to connect your Fly.io application to pull secrets from Infisical.", + category: "Hosting", + }, + { + name: "Render", + slug: "render", + path: "/integrations/app-connections/render", + description: + "Learn how to connect your Render application to pull secrets from Infisical.", + category: "Hosting", + }, + { + name: "Heroku", + slug: "heroku", + path: "/integrations/app-connections/heroku", + description: + "Learn how to connect your Heroku application to pull secrets from Infisical.", + category: "Hosting", + }, + { + name: "DigitalOcean", + slug: "digital-ocean", + path: "/integrations/app-connections/digital-ocean", + description: + "Learn how to connect your DigitalOcean application to pull secrets from Infisical.", + category: "Hosting", + }, + { + name: "Supabase", + slug: "supabase", + path: "/integrations/app-connections/supabase", + description: + "Learn how to connect your Supabase application to pull secrets from Infisical.", + category: "Databases", + }, + { + name: "Checkly", + slug: "checkly", + path: "/integrations/app-connections/checkly", + description: + "Learn how to connect your Checkly application to pull secrets from Infisical.", + category: "Monitoring", + }, + { + name: "GitHub", + slug: "github", + path: "/integrations/app-connections/github", + description: + "Learn how to connect your GitHub application to pull secrets from Infisical.", + category: "CI/CD", + }, + { + name: "GitHub Radar", + slug: "github-radar", + path: "/integrations/app-connections/github-radar", + description: + "Learn how to connect your GitHub Radar to pull secrets from Infisical.", + category: "CI/CD", + }, + { + name: "GitLab", + slug: "gitlab", + path: "/integrations/app-connections/gitlab", + description: + "Learn how to connect your GitLab application to pull secrets from Infisical.", + category: "CI/CD", + }, + { + name: "TeamCity", + slug: "teamcity", + path: "/integrations/app-connections/teamcity", + description: + "Learn how to connect your TeamCity to pull secrets from Infisical.", + category: "CI/CD", + }, + { + name: "Bitbucket", + slug: "bitbucket", + path: "/integrations/app-connections/bitbucket", + description: + "Learn how to connect your Bitbucket to pull secrets from Infisical.", + category: "CI/CD", + }, + { + name: "Terraform Cloud", + slug: "terraform-cloud", + path: "/integrations/app-connections/terraform-cloud", + description: + "Learn how to connect your Terraform Cloud to pull secrets from Infisical.", + category: "DevOps Tools", + }, + { + name: "Cloudflare", + slug: "cloudflare", + path: "/integrations/app-connections/cloudflare", + description: + "Learn how to connect your Cloudflare application to pull secrets from Infisical.", + category: "Cloud Providers", + }, + { + name: "Databricks", + slug: "databricks", + path: "/integrations/app-connections/databricks", + description: + "Learn how to connect your Databricks to pull secrets from Infisical.", + category: "Data Analytics", + }, + { + name: "DNS Made Easy", + slug: "dns-made-easy", + path: "/integrations/app-connections/dns-made-easy", + description: "Learn how to connect Infisical to DNS Made Easy.", + category: "Networking & DNS", + }, + { + name: "Windmill", + slug: "windmill", + path: "/integrations/app-connections/windmill", + description: + "Learn how to connect your Windmill to pull secrets from Infisical.", + category: "DevOps Tools", + }, + { + name: "Camunda", + slug: "camunda", + path: "/integrations/app-connections/camunda", + description: + "Learn how to connect your Camunda to pull secrets from Infisical.", + category: "DevOps Tools", + }, + { + name: "Humanitec", + slug: "humanitec", + path: "/integrations/app-connections/humanitec", + description: + "Learn how to connect your Humanitec to pull secrets from Infisical.", + category: "DevOps Tools", + }, + { + name: "OCI", + slug: "oci", + path: "/integrations/app-connections/oci", + description: + "Learn how to connect your OCI applications to pull secrets from Infisical.", + category: "Cloud Providers", + }, + { + name: "Zabbix", + slug: "zabbix", + path: "/integrations/app-connections/zabbix", + description: + "Learn how to connect your Zabbix to pull secrets from Infisical.", + category: "Monitoring", + }, + { + name: "MySQL", + slug: "mysql", + path: "/integrations/app-connections/mysql", + description: + "Learn how to connect your MySQL database to pull secrets from Infisical.", + category: "Databases", + }, + { + name: "PostgreSQL", + slug: "postgres", + path: "/integrations/app-connections/postgres", + description: + "Learn how to connect your PostgreSQL database to pull secrets from Infisical.", + category: "Databases", + }, + { + name: "Microsoft SQL Server", + slug: "mssql", + path: "/integrations/app-connections/mssql", + description: + "Learn how to connect your SQL Server database to pull secrets from Infisical.", + category: "Databases", + }, + { + name: "Oracle Database", + slug: "oracledb", + path: "/integrations/app-connections/oracledb", + description: + "Learn how to connect your Oracle database to pull secrets from Infisical.", + category: "Databases", + }, + { + name: "Redis", + slug: "redis", + path: "/integrations/app-connections/redis", + description: "Learn how to connect Redis to pull secrets from Infisical.", + category: "Databases", + }, + { + name: "LDAP", + slug: "ldap", + path: "/integrations/app-connections/ldap", + description: + "Learn how to connect your LDAP to pull secrets from Infisical.", + category: "Directory Services", + }, + { + name: "Auth0", + slug: "auth0", + path: "/integrations/app-connections/auth0", + description: + "Learn how to connect your Auth0 to pull secrets from Infisical.", + category: "Identity & Auth", + }, + { + name: "Okta", + slug: "okta", + path: "/integrations/app-connections/okta", + description: + "Learn how to connect your Okta to pull secrets from Infisical.", + category: "Identity & Auth", + }, + { + name: "Laravel Forge", + slug: "laravel-forge", + path: "/integrations/app-connections/laravel-forge", + description: + "Learn how to connect your Laravel Forge to pull secrets from Infisical.", + category: "Hosting", + }, + { + name: "Chef", + slug: "chef", + path: "/integrations/app-connections/chef", + description: + "Learn how to connect your Chef to pull secrets from Infisical.", + category: "DevOps Tools", + }, + { + name: "Northflank", + slug: "northflank", + path: "/integrations/app-connections/northflank", + description: + "Learn how to connect your Northflank projects to pull secrets from Infisical.", + category: "Hosting", + }, + ].sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); const filteredConnections = useMemo(() => { let filtered = connections; - - if (selectedCategory !== 'All') { - filtered = filtered.filter(connection => connection.category === selectedCategory); + + if (selectedCategory !== "All") { + filtered = filtered.filter( + (connection) => connection.category === selectedCategory + ); } if (searchTerm) { - filtered = filtered.filter(connection => - connection.name.toLowerCase().includes(searchTerm.toLowerCase()) || - connection.description.toLowerCase().includes(searchTerm.toLowerCase()) || - connection.category.toLowerCase().includes(searchTerm.toLowerCase()) + filtered = filtered.filter( + (connection) => + connection.name.toLowerCase().includes(searchTerm.toLowerCase()) || + connection.description + .toLowerCase() + .includes(searchTerm.toLowerCase()) || + connection.category.toLowerCase().includes(searchTerm.toLowerCase()) ); } @@ -77,8 +395,18 @@ export const AppConnectionsBrowser = () => {
- - + +
{ {/* Category Filter */}
- {categories.map(category => ( + {categories.map((category) => (
); -}; \ No newline at end of file +}; diff --git a/frontend/public/images/integrations/DNSMadeEasy.svg b/frontend/public/images/integrations/DNSMadeEasy.svg new file mode 100644 index 000000000..be77b9840 --- /dev/null +++ b/frontend/public/images/integrations/DNSMadeEasy.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index fa23b9630..a290a28a3 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -289,7 +289,7 @@ } }, "project": { - "title": "Settings", + "title": "Project Settings", "description": "These settings only apply to the currently selected Project.", "danger-zone": "Danger Zone", "delete-project": "Delete Project", diff --git a/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts b/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts index a40398ba7..b05ea1c52 100644 --- a/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts +++ b/frontend/src/components/permissions/AccessTree/utils/createFolderNode.ts @@ -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 = ( diff --git a/frontend/src/components/v2/PageHeader/PageHeader.tsx b/frontend/src/components/v2/PageHeader/PageHeader.tsx index f84edfcd2..5a2ba2e12 100644 --- a/frontend/src/components/v2/PageHeader/PageHeader.tsx +++ b/frontend/src/components/v2/PageHeader/PageHeader.tsx @@ -3,14 +3,7 @@ import { ReactNode } from "@tanstack/react-router"; import { LucideIcon } from "lucide-react"; import { twMerge } from "tailwind-merge"; -import { - Badge, - InstanceIcon, - OrgIcon, - ProjectIcon, - SubOrgIcon, - TBadgeProps -} from "@app/components/v3"; +import { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon } from "@app/components/v3"; import { ProjectType } from "@app/hooks/api/projects/types"; type Props = { @@ -21,41 +14,40 @@ type Props = { scope: "org" | "namespace" | "instance" | ProjectType | null; }; -const SCOPE_NAME: Record, { label: string; icon: LucideIcon }> = { - org: { label: "Organization", icon: OrgIcon }, - [ProjectType.SecretManager]: { label: "Project", icon: ProjectIcon }, - [ProjectType.CertificateManager]: { label: "Project", icon: ProjectIcon }, - [ProjectType.SSH]: { label: "Project", icon: ProjectIcon }, - [ProjectType.KMS]: { label: "Project", icon: ProjectIcon }, - [ProjectType.PAM]: { label: "Project", icon: ProjectIcon }, - [ProjectType.SecretScanning]: { label: "Project", icon: ProjectIcon }, - namespace: { label: "Sub-Organization", icon: SubOrgIcon }, - instance: { label: "Server", icon: InstanceIcon } -}; - -const SCOPE_VARIANT: Record, TBadgeProps["variant"]> = { - org: "org", - [ProjectType.SecretManager]: "project", - [ProjectType.CertificateManager]: "project", - [ProjectType.SSH]: "project", - [ProjectType.KMS]: "project", - [ProjectType.PAM]: "project", - [ProjectType.SecretScanning]: "project", - namespace: "sub-org", - instance: "neutral" +const SCOPE_BADGE: Record, { icon: LucideIcon; className: string }> = { + org: { className: "text-org", icon: OrgIcon }, + [ProjectType.SecretManager]: { className: "text-project", icon: ProjectIcon }, + [ProjectType.CertificateManager]: { className: "text-project", icon: ProjectIcon }, + [ProjectType.SSH]: { className: "text-project", icon: ProjectIcon }, + [ProjectType.KMS]: { className: "text-project", icon: ProjectIcon }, + [ProjectType.PAM]: { className: "text-project", icon: ProjectIcon }, + [ProjectType.SecretScanning]: { className: "text-project", icon: ProjectIcon }, + namespace: { className: "text-sub-org", icon: SubOrgIcon }, + instance: { className: "text-neutral", icon: InstanceIcon } }; export const PageHeader = ({ title, description, children, className, scope }: Props) => (
-

{title}

- {scope && ( - - {createElement(SCOPE_NAME[scope].icon)} - {SCOPE_NAME[scope].label} - - )} +

+ {scope && + createElement(SCOPE_BADGE[scope].icon, { + size: 26, + className: twMerge(SCOPE_BADGE[scope].className, "mr-3 mb-1 inline-block") + })} + {title} +

{children}
diff --git a/frontend/src/components/v2/Tabs/Tabs.tsx b/frontend/src/components/v2/Tabs/Tabs.tsx index ebe8eb2ed..47b3d08cd 100644 --- a/frontend/src/components/v2/Tabs/Tabs.tsx +++ b/frontend/src/components/v2/Tabs/Tabs.tsx @@ -47,8 +47,8 @@ export const Tab = ({ }) => ( + [...dnsMadeEasyConnectionKeys.all, "zones", connectionId] as const +}; + +export const useDNSMadeEasyConnectionListZones = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TDNSMadeEasyZone[], + unknown, + TDNSMadeEasyZone[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: dnsMadeEasyConnectionKeys.listZones(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/dns-made-easy/${connectionId}/dns-made-easy-zones` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/dns-made-easy/types.ts b/frontend/src/hooks/api/appConnections/dns-made-easy/types.ts new file mode 100644 index 000000000..dcf66c1dd --- /dev/null +++ b/frontend/src/hooks/api/appConnections/dns-made-easy/types.ts @@ -0,0 +1,4 @@ +export type TDNSMadeEasyZone = { + id: string; + name: string; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index fba0cbb4b..9535e8348 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -29,6 +29,7 @@ export enum AppConnection { Flyio = "flyio", GitLab = "gitlab", Cloudflare = "cloudflare", + DNSMadeEasy = "dns-made-easy", Bitbucket = "bitbucket", Zabbix = "zabbix", Railway = "railway", diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 1f553f605..d1c991f34 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -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; diff --git a/frontend/src/hooks/api/appConnections/types/dns-made-easy-connection.ts b/frontend/src/hooks/api/appConnections/types/dns-made-easy-connection.ts new file mode 100644 index 000000000..fd4dc098b --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/dns-made-easy-connection.ts @@ -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; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 3e62031b3..a272b48cd 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -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"; @@ -57,6 +58,7 @@ export * from "./checkly-connection"; export * from "./chef-connection"; export * from "./cloudflare-connection"; export * from "./databricks-connection"; +export * from "./dns-made-easy-connection"; export * from "./flyio-connection"; export * from "./gcp-connection"; export * from "./github-connection"; @@ -127,7 +129,8 @@ export type TAppConnection = | TNorthflankConnection | TOktaConnection | TRedisConnection - | TChefConnection; + | TChefConnection + | TDNSMadeEasyConnection; export type TAvailableAppConnection = Pick; diff --git a/frontend/src/hooks/api/ca/constants.tsx b/frontend/src/hooks/api/ca/constants.tsx index 16a350dbd..740d52994 100644 --- a/frontend/src/hooks/api/ca/constants.tsx +++ b/frontend/src/hooks/api/ca/constants.tsx @@ -16,12 +16,14 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = { export const ACME_DNS_PROVIDER_NAME_MAP: Record = { [AcmeDnsProvider.ROUTE53]: "Route53", - [AcmeDnsProvider.Cloudflare]: "Cloudflare" + [AcmeDnsProvider.Cloudflare]: "Cloudflare", + [AcmeDnsProvider.DNSMadeEasy]: "DNS Made Easy" }; export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record = { [AcmeDnsProvider.ROUTE53]: AppConnection.AWS, - [AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare + [AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare, + [AcmeDnsProvider.DNSMadeEasy]: AppConnection.DNSMadeEasy }; export const CA_TYPE_CAPABILITIES_MAP: Record = { diff --git a/frontend/src/hooks/api/ca/enums.tsx b/frontend/src/hooks/api/ca/enums.tsx index a68f3e862..acd8ce069 100644 --- a/frontend/src/hooks/api/ca/enums.tsx +++ b/frontend/src/hooks/api/ca/enums.tsx @@ -21,7 +21,8 @@ export enum CaRenewalType { export enum AcmeDnsProvider { ROUTE53 = "route53", - Cloudflare = "cloudflare" + Cloudflare = "cloudflare", + DNSMadeEasy = "dns-made-easy" } export enum CaCapability { diff --git a/frontend/src/hooks/api/ca/index.tsx b/frontend/src/hooks/api/ca/index.tsx index 05a161c75..04a44dbd6 100644 --- a/frontend/src/hooks/api/ca/index.tsx +++ b/frontend/src/hooks/api/ca/index.tsx @@ -13,12 +13,12 @@ export { export { useGetAzureAdcsTemplates, useGetCa, - useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCertTemplates, useGetCaCrls, useGetCaCsr, + useGetInternalCaById, useListCasByProjectId, useListCasByTypeAndProjectId, useListExternalCasByProjectId diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index e19069984..e87e1cf2d 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -27,21 +27,20 @@ import { export const useUpdateCa = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ caName, ...body }) => { + mutationFn: async ({ id, ...body }) => { const { data } = await apiRequest.patch( - `/api/v1/pki/ca/${body.type}/${caName}`, + `/api/v1/cert-manager/ca/${body.type}/${id}`, body ); return data; }, - onSuccess: ({ projectId, type }, { caName }) => { - caKeys.getCaByNameAndProjectId(caName, projectId); + onSuccess: ({ projectId, type }, { id }) => { queryClient.invalidateQueries({ queryKey: caKeys.listCasByTypeAndProjectId(type, projectId) }); queryClient.invalidateQueries({ - queryKey: caKeys.getCaByNameAndProjectId(caName, projectId) + queryKey: caKeys.getCaById(id) }); // Invalidate external CAs list queryClient.invalidateQueries({ @@ -56,7 +55,7 @@ export const useCreateCa = () => { return useMutation({ mutationFn: async (body) => { const { data } = await apiRequest.post( - `/api/v1/pki/ca/${body.type}`, + `/api/v1/cert-manager/ca/${body.type}`, body ); return data; @@ -76,14 +75,9 @@ export const useCreateCa = () => { export const useDeleteCa = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ caName, type, projectId }) => { + mutationFn: async ({ id, type }) => { const { data } = await apiRequest.delete( - `/api/v1/pki/ca/${type}/${caName}`, - { - data: { - projectId - } - } + `/api/v1/cert-manager/ca/${type}/${id}` ); return data; }, @@ -104,7 +98,7 @@ export const useSignIntermediate = () => { return useMutation({ mutationFn: async (body) => { const { data } = await apiRequest.post( - `/api/v1/pki/ca/${body.caId}/sign-intermediate`, + `/api/v1/cert-manager/ca/internal/${body.caId}/sign-intermediate`, body ); return data; @@ -117,13 +111,14 @@ export const useImportCaCertificate = (projectId: string) => { return useMutation({ mutationFn: async ({ caId, ...body }) => { const { data } = await apiRequest.post( - `/api/v1/pki/ca/${caId}/import-certificate`, + `/api/v1/cert-manager/ca/internal/${caId}/import-certificate`, body ); return data; }, onSuccess: (_, { caId }) => { queryClient.invalidateQueries({ queryKey: projectKeys.getProjectCas({ projectId }) }); + queryClient.invalidateQueries({ queryKey: caKeys.getCaById(caId) }); queryClient.invalidateQueries({ queryKey: caKeys.getCaCerts(caId) }); queryClient.invalidateQueries({ queryKey: caKeys.getCaCert(caId) }); queryClient.invalidateQueries({ @@ -133,7 +128,7 @@ export const useImportCaCertificate = (projectId: string) => { }); }; -// consider rename to issue certificate +// TODO: DEPRECATE export const useCreateCertificate = () => { const queryClient = useQueryClient(); return useMutation({ @@ -157,7 +152,7 @@ export const useCreateCertificateV3 = (options?: { projectId?: string }) => { return useMutation({ mutationFn: async (body) => { const { data } = await apiRequest.post( - "/api/v3/pki/certificates/issue-certificate", + "/api/v1/cert-manager/certificates/issue-certificate", body ); return data; @@ -185,7 +180,7 @@ export const useOrderCertificateWithProfile = () => { return useMutation({ mutationFn: async (body) => { const { data } = await apiRequest.post( - "/api/v3/pki/certificates/order-certificate", + "/api/v1/cert-manager/certificates/order-certificate", body ); return data; @@ -203,7 +198,7 @@ export const useRenewCa = () => { return useMutation({ mutationFn: async (body) => { const { data } = await apiRequest.post( - `/api/v1/pki/ca/${body.caId}/renew`, + `/api/v1/cert-manager/ca/internal/${body.caId}/renew`, body ); return data; diff --git a/frontend/src/hooks/api/ca/queries.tsx b/frontend/src/hooks/api/ca/queries.tsx index 68e8d2c12..09e4c0f3b 100644 --- a/frontend/src/hooks/api/ca/queries.tsx +++ b/frontend/src/hooks/api/ca/queries.tsx @@ -4,7 +4,11 @@ import { apiRequest } from "@app/config/request"; import { TCertificateTemplate } from "../certificateTemplates/types"; import { CaType } from "./enums"; -import { TAzureAdCsTemplate, TCertificateAuthority, TUnifiedCertificateAuthority } from "./types"; +import { + TAzureAdCsTemplate, + TInternalCertificateAuthority, + TUnifiedCertificateAuthority +} from "./types"; export const caKeys = { getCaById: (caId: string) => [{ caId }, "ca"], @@ -25,24 +29,16 @@ export const caKeys = { ] }; -export const useGetCa = ({ - caName, - projectId, - type -}: { - caName: string; - projectId: string; - type: CaType; -}) => { +export const useGetCa = ({ caId, type }: { caId: string; type: CaType }) => { return useQuery({ - queryKey: caKeys.getCaByNameAndProjectId(caName, projectId), + queryKey: caKeys.getCaById(caId), queryFn: async () => { const { data } = await apiRequest.get( - `/api/v1/pki/ca/${type}/${caName}?projectId=${projectId}` + `/api/v1/cert-manager/ca/${type}/${caId}` ); return data; }, - enabled: Boolean(caName && projectId && type) + enabled: Boolean(caId && type) }); }; @@ -51,7 +47,7 @@ export const useListCasByTypeAndProjectId = (type: CaType, projectId: string) => queryKey: caKeys.listCasByTypeAndProjectId(type, projectId), queryFn: async () => { const { data } = await apiRequest.get( - `/api/v1/pki/ca/${type}?projectId=${projectId}` + `/api/v1/cert-manager/ca/${type}?projectId=${projectId}` ); return data; @@ -65,7 +61,7 @@ export const useListCasByProjectId = (projectId: string) => { queryFn: async () => { const { data } = await apiRequest.get<{ certificateAuthorities: TUnifiedCertificateAuthority[]; - }>(`/api/v2/pki/ca?projectId=${projectId}`); + }>(`/api/v1/cert-manager/ca?projectId=${projectId}`); return data.certificateAuthorities; } @@ -78,10 +74,10 @@ export const useListExternalCasByProjectId = (projectId: string) => { queryFn: async () => { const [acmeResponse, azureAdCsResponse] = await Promise.allSettled([ apiRequest.get( - `/api/v1/pki/ca/${CaType.ACME}?projectId=${projectId}` + `/api/v1/cert-manager/ca/${CaType.ACME}?projectId=${projectId}` ), apiRequest.get( - `/api/v1/pki/ca/${CaType.AZURE_AD_CS}?projectId=${projectId}` + `/api/v1/cert-manager/ca/${CaType.AZURE_AD_CS}?projectId=${projectId}` ) ]); @@ -100,14 +96,14 @@ export const useListExternalCasByProjectId = (projectId: string) => { }); }; -export const useGetCaById = (caId: string) => { +export const useGetInternalCaById = (caId: string) => { return useQuery({ queryKey: caKeys.getCaById(caId), queryFn: async () => { - const { - data: { ca } - } = await apiRequest.get<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`); - return ca; + const { data } = await apiRequest.get( + `/api/v1/cert-manager/ca/internal/${caId}` + ); + return data; }, enabled: Boolean(caId) }); @@ -124,7 +120,7 @@ export const useGetCaCerts = (caId: string) => { serialNumber: string; version: number; }[] - >(`/api/v1/pki/ca/${caId}/ca-certificates`); // TODO: consider updating endpoint structure + >(`/api/v1/cert-manager/ca/internal/${caId}/ca-certificates`); return data; }, enabled: Boolean(caId) @@ -139,7 +135,7 @@ export const useGetCaCert = (caId: string) => { certificate: string; certificateChain: string; serialNumber: string; - }>(`/api/v1/pki/ca/${caId}/certificate`); // TODO: consider updating endpoint structure + }>(`/api/v1/cert-manager/ca/internal/${caId}/certificate`); return data; }, enabled: Boolean(caId) @@ -154,7 +150,7 @@ export const useGetCaCsr = (caId: string) => { data: { csr } } = await apiRequest.get<{ csr: string; - }>(`/api/v1/pki/ca/${caId}/csr`); + }>(`/api/v1/cert-manager/ca/internal/${caId}/csr`); return csr; }, enabled: Boolean(caId) @@ -170,13 +166,14 @@ export const useGetCaCrls = (caId: string) => { id: string; crl: string; }[] - >(`/api/v1/pki/ca/${caId}/crls`); + >(`/api/v1/cert-manager/ca/internal/${caId}/crls`); return data; }, enabled: Boolean(caId) }); }; +// TODO: DEPRECATE export const useGetCaCertTemplates = (caId: string) => { return useQuery({ queryKey: caKeys.getCaCertTemplates(caId), @@ -202,7 +199,7 @@ export const useGetAzureAdcsTemplates = ({ queryFn: async () => { const { data } = await apiRequest.get<{ templates: TAzureAdCsTemplate[]; - }>(`/api/v1/pki/ca/azure-ad-cs/${caId}/templates?projectId=${projectId}`); + }>(`/api/v1/cert-manager/ca/azure-ad-cs/${caId}/templates?projectId=${projectId}`); return data; }, enabled: Boolean(caId && projectId) diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index 31d35e904..57af49259 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -66,17 +66,19 @@ export type TUnifiedCertificateAuthority = | TAzureAdCsCertificateAuthority | TInternalCertificateAuthority; -export type TCreateCertificateAuthorityDTO = Omit; +export type TCreateCertificateAuthorityDTO = Omit< + TUnifiedCertificateAuthority, + "id" | "enableDirectIssuance" +>; export type TUpdateCertificateAuthorityDTO = Partial & { - caName: string; - projectId: string; + id: string; type: CaType; }; export type TDeleteCertificateAuthorityDTO = { - caName: string; - type: CaType; + id: string; projectId: string; + type: CaType; }; export type TCertificateAuthority = { diff --git a/frontend/src/hooks/api/certificateProfiles/mutations.tsx b/frontend/src/hooks/api/certificateProfiles/mutations.tsx index ca784ed0d..8cfb04b76 100644 --- a/frontend/src/hooks/api/certificateProfiles/mutations.tsx +++ b/frontend/src/hooks/api/certificateProfiles/mutations.tsx @@ -17,7 +17,7 @@ export const useCreateCertificateProfile = () => { mutationFn: async (data) => { const { data: response } = await apiRequest.post<{ certificateProfile: TCertificateProfile; - }>("/api/v1/pki/certificate-profiles", data); + }>("/api/v1/cert-manager/certificate-profiles", data); return response.certificateProfile; }, onSuccess: (_, { projectId }) => { @@ -35,7 +35,7 @@ export const useUpdateCertificateProfile = () => { mutationFn: async ({ profileId, ...data }) => { const { data: response } = await apiRequest.patch<{ certificateProfile: TCertificateProfile; - }>(`/api/v1/pki/certificate-profiles/${profileId}`, data); + }>(`/api/v1/cert-manager/certificate-profiles/${profileId}`, data); return response.certificateProfile; }, onSuccess: (profile, { profileId }) => { @@ -56,7 +56,7 @@ export const useDeleteCertificateProfile = () => { mutationFn: async ({ profileId }) => { const { data: response } = await apiRequest.delete<{ certificateProfile: TCertificateProfile; - }>(`/api/v1/pki/certificate-profiles/${profileId}`); + }>(`/api/v1/cert-manager/certificate-profiles/${profileId}`); return response.certificateProfile; }, onSuccess: (profile, { profileId }) => { diff --git a/frontend/src/hooks/api/certificateProfiles/queries.tsx b/frontend/src/hooks/api/certificateProfiles/queries.tsx index 1e0fe3b9b..71f0e6ce9 100644 --- a/frontend/src/hooks/api/certificateProfiles/queries.tsx +++ b/frontend/src/hooks/api/certificateProfiles/queries.tsx @@ -71,7 +71,7 @@ export const useListCertificateProfiles = ({ const { data } = await apiRequest.get<{ certificateProfiles: TCertificateProfile[]; totalCount: number; - }>("/api/v1/pki/certificate-profiles", { + }>("/api/v1/cert-manager/certificate-profiles", { params: { projectId, limit, @@ -93,7 +93,7 @@ export const useGetCertificateProfileById = ({ profileId }: TGetCertificateProfi queryFn: async () => { const { data } = await apiRequest.get<{ certificateProfile: TCertificateProfileWithDetails; - }>(`/api/v1/pki/certificate-profiles/${profileId}`); + }>(`/api/v1/cert-manager/certificate-profiles/${profileId}`); return data.certificateProfile; }, enabled: Boolean(profileId) @@ -109,7 +109,7 @@ export const useGetCertificateProfileBySlug = ({ queryFn: async () => { const { data } = await apiRequest.get<{ certificateProfile: TCertificateProfile; - }>(`/api/v1/pki/certificate-profiles/slug/${slug}`, { + }>(`/api/v1/cert-manager/certificate-profiles/slug/${slug}`, { params: { projectId } }); return data.certificateProfile; @@ -125,7 +125,7 @@ export const useRevealAcmeEabSecret = ({ profileId }: TRevealAcmeEabSecretDTO) = const { data } = await apiRequest.get<{ eabKid: string; eabSecret: string; - }>(`/api/v1/pki/certificate-profiles/${profileId}/acme/eab-secret/reveal`); + }>(`/api/v1/cert-manager/certificate-profiles/${profileId}/acme/eab-secret/reveal`); return data; }, enabled: Boolean(profileId) @@ -144,7 +144,7 @@ export const useGetProfileCertificates = ({ queryFn: async () => { const { data } = await apiRequest.get<{ certificates: TProfileCertificate[]; - }>(`/api/v1/pki/certificate-profiles/${profileId}/certificates`, { + }>(`/api/v1/cert-manager/certificate-profiles/${profileId}/certificates`, { params: { offset, limit, diff --git a/frontend/src/hooks/api/certificateTemplates/mutations.tsx b/frontend/src/hooks/api/certificateTemplates/mutations.tsx index 998194ddc..0acebae52 100644 --- a/frontend/src/hooks/api/certificateTemplates/mutations.tsx +++ b/frontend/src/hooks/api/certificateTemplates/mutations.tsx @@ -21,6 +21,7 @@ import { TUpdateEstConfigDTO } from "./types"; +// TODO: DEPRECATE export const useCreateCertTemplate = () => { const queryClient = useQueryClient(); return useMutation({ @@ -40,6 +41,7 @@ export const useCreateCertTemplate = () => { }); }; +// TODO: DEPRECATE export const useUpdateCertTemplate = () => { const queryClient = useQueryClient(); return useMutation({ @@ -61,6 +63,7 @@ export const useUpdateCertTemplate = () => { }); }; +// TODO: DEPRECATE export const useDeleteCertTemplate = () => { const queryClient = useQueryClient(); return useMutation({ @@ -147,6 +150,7 @@ export const useDeleteCertTemplateV2 = () => { }); }; +// TODO: DEPRECATE export const useCreateEstConfig = () => { const queryClient = useQueryClient(); return useMutation({ @@ -165,6 +169,7 @@ export const useCreateEstConfig = () => { }); }; +// TODO: DEPRECATE export const useUpdateEstConfig = () => { const queryClient = useQueryClient(); return useMutation({ @@ -193,7 +198,7 @@ export const useCreateCertificateTemplateV2WithPolicies = () => { mutationFn: async (data) => { const { data: response } = await apiRequest.post<{ certificateTemplate: TCertificateTemplateV2WithPolicies; - }>("/api/v2/certificate-templates", data); + }>("/api/v1/cert-manager/certificate-templates", data); return response.certificateTemplate; }, onSuccess: (_, { projectId }) => { @@ -214,7 +219,7 @@ export const useUpdateCertificateTemplateV2WithPolicies = () => { mutationFn: async ({ templateId, ...data }) => { const { data: response } = await apiRequest.patch<{ certificateTemplate: TCertificateTemplateV2WithPolicies; - }>(`/api/v2/certificate-templates/${templateId}`, data); + }>(`/api/v1/cert-manager/certificate-templates/${templateId}`, data); return response.certificateTemplate; }, onSuccess: (template, { templateId }) => { @@ -238,7 +243,7 @@ export const useDeleteCertificateTemplateV2WithPolicies = () => { mutationFn: async ({ templateId }) => { const { data: response } = await apiRequest.delete<{ certificateTemplate: TCertificateTemplateV2WithPolicies; - }>(`/api/v2/certificate-templates/${templateId}`); + }>(`/api/v1/cert-manager/certificate-templates/${templateId}`); return response.certificateTemplate; }, onSuccess: (template, { templateId }) => { diff --git a/frontend/src/hooks/api/certificateTemplates/queries.tsx b/frontend/src/hooks/api/certificateTemplates/queries.tsx index 383f4ed71..78c1ebd81 100644 --- a/frontend/src/hooks/api/certificateTemplates/queries.tsx +++ b/frontend/src/hooks/api/certificateTemplates/queries.tsx @@ -31,6 +31,7 @@ export const certTemplateKeys = { getTemplateV2ById: (id: string) => ["cert-template-v2", id] }; +// TODO: DEPRECATE export const useGetCertTemplate = (id: string) => { return useQuery({ queryKey: certTemplateKeys.getCertTemplateById(id), @@ -44,6 +45,7 @@ export const useGetCertTemplate = (id: string) => { }); }; +// TODO: DEPRECATE export const useListCertificateTemplates = ({ limit = 100, offset = 0, @@ -67,6 +69,7 @@ export const useListCertificateTemplates = ({ }); }; +// TODO: DEPRECATE export const useGetEstConfig = (certificateTemplateId: string) => { return useQuery({ queryKey: certTemplateKeys.getEstConfig(certificateTemplateId), @@ -92,7 +95,7 @@ export const useListCertificateTemplatesV2 = ({ const { data } = await apiRequest.get<{ certificateTemplates: TCertificateTemplateV2WithPolicies[]; totalCount: number; - }>("/api/v2/certificate-templates", { + }>("/api/v1/cert-manager/certificate-templates", { params: { projectId, limit, @@ -113,7 +116,7 @@ export const useGetCertificateTemplateV2ById = ({ queryFn: async () => { const { data } = await apiRequest.get<{ certificateTemplate: TCertificateTemplateV2WithPolicies; - }>(`/api/v2/certificate-templates/${templateId}`); + }>(`/api/v1/cert-manager/certificate-templates/${templateId}`); return data.certificateTemplate; }, enabled: Boolean(templateId) diff --git a/frontend/src/hooks/api/certificates/mutations.tsx b/frontend/src/hooks/api/certificates/mutations.tsx index 871d06bab..e64b87d5d 100644 --- a/frontend/src/hooks/api/certificates/mutations.tsx +++ b/frontend/src/hooks/api/certificates/mutations.tsx @@ -21,11 +21,11 @@ import { export const useDeleteCert = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ serialNumber }) => { + mutationFn: async ({ id }) => { const { data: { certificate } } = await apiRequest.delete<{ certificate: TCertificate }>( - `/api/v1/pki/certificates/${serialNumber}` + `/api/v1/cert-manager/certificates/${id}` ); return certificate; }, @@ -49,11 +49,11 @@ export const useDeleteCert = () => { export const useRevokeCert = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ serialNumber, revocationReason }) => { + mutationFn: async ({ id, revocationReason }) => { const { data: { certificate } } = await apiRequest.post<{ certificate: TCertificate }>( - `/api/v1/pki/certificates/${serialNumber}/revoke`, + `/api/v1/cert-manager/certificates/${id}/revoke`, { revocationReason } @@ -82,7 +82,7 @@ export const useImportCertificate = () => { return useMutation({ mutationFn: async (body) => { const { data } = await apiRequest.post( - "/api/v1/pki/certificates/import-certificate", + "/api/v1/cert-manager/certificates/import-certificate", body ); return data; @@ -100,7 +100,7 @@ export const useRenewCertificate = () => { return useMutation({ mutationFn: async ({ certificateId }) => { const { data } = await apiRequest.post( - `/api/v3/pki/certificates/${certificateId}/renew`, + `/api/v1/cert-manager/certificates/${certificateId}/renew`, {} ); return data; @@ -133,7 +133,7 @@ export const useUpdateRenewalConfig = () => { >({ mutationFn: async ({ certificateId, renewBeforeDays, enableAutoRenewal }) => { const { data } = await apiRequest.patch<{ message: string; renewBeforeDays?: number }>( - `/api/v3/pki/certificates/${certificateId}/config`, + `/api/v1/cert-manager/certificates/${certificateId}/config`, { renewBeforeDays, enableAutoRenewal } ); return data; @@ -151,10 +151,10 @@ export const useUpdateRenewalConfig = () => { export const useDownloadCertPkcs12 = () => { return useMutation({ - mutationFn: async ({ serialNumber, projectSlug, password, alias }) => { + mutationFn: async ({ certificateId, projectSlug, password, alias }) => { try { const response = await apiRequest.post( - `/api/v1/pki/certificates/${serialNumber}/pkcs12`, + `/api/v1/cert-manager/certificates/${certificateId}/pkcs12`, { password, alias @@ -170,7 +170,7 @@ export const useDownloadCertPkcs12 = () => { const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; - link.download = `certificate-${serialNumber}.p12`; + link.download = `certificate-${certificateId}.p12`; document.body.appendChild(link); link.click(); document.body.removeChild(link); diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index 27cb060b3..2d32a007b 100644 --- a/frontend/src/hooks/api/certificates/types.ts +++ b/frontend/src/hooks/api/certificates/types.ts @@ -24,13 +24,13 @@ export type TCertificate = { }; export type TDeleteCertDTO = { + id: string; projectId: string; - serialNumber: string; }; export type TRevokeCertDTO = { projectId: string; - serialNumber: string; + id: string; revocationReason: string; }; @@ -75,7 +75,7 @@ export type TUpdateRenewalConfigDTO = { }; export type TDownloadPkcs12DTO = { - serialNumber: string; + certificateId: string; projectSlug: string; password: string; alias: string; diff --git a/frontend/src/hooks/api/pkiAlerts/mutations.tsx b/frontend/src/hooks/api/pkiAlerts/mutations.tsx index df48a6aab..5c41dd619 100644 --- a/frontend/src/hooks/api/pkiAlerts/mutations.tsx +++ b/frontend/src/hooks/api/pkiAlerts/mutations.tsx @@ -6,6 +6,7 @@ import { projectKeys } from "../projects"; import { pkiAlertKeys } from "./queries"; import { TCreatePkiAlertDTO, TDeletePkiAlertDTO, TPkiAlert, TUpdatePkiAlertDTO } from "./types"; +// TODO: DEPRECATE export const useCreatePkiAlert = () => { const queryClient = useQueryClient(); return useMutation({ @@ -19,6 +20,7 @@ export const useCreatePkiAlert = () => { }); }; +// TODO: DEPRECATE export const useUpdatePkiAlert = () => { const queryClient = useQueryClient(); return useMutation({ @@ -36,6 +38,7 @@ export const useUpdatePkiAlert = () => { }); }; +// TODO: DEPRECATE export const useDeletePkiAlert = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/hooks/api/pkiAlerts/queries.tsx b/frontend/src/hooks/api/pkiAlerts/queries.tsx index db324e96d..01db5ea51 100644 --- a/frontend/src/hooks/api/pkiAlerts/queries.tsx +++ b/frontend/src/hooks/api/pkiAlerts/queries.tsx @@ -8,6 +8,7 @@ export const pkiAlertKeys = { getPkiAlertById: (alertId: string) => [{ alertId }, "alert"] }; +// TODO: DEPRECATE export const useGetPkiAlertById = (alertId: string) => { return useQuery({ queryKey: pkiAlertKeys.getPkiAlertById(alertId), diff --git a/frontend/src/hooks/api/pkiAlertsV2/mutations.ts b/frontend/src/hooks/api/pkiAlertsV2/mutations.ts index 7092cf7b5..e9159550f 100644 --- a/frontend/src/hooks/api/pkiAlertsV2/mutations.ts +++ b/frontend/src/hooks/api/pkiAlertsV2/mutations.ts @@ -11,7 +11,7 @@ export const useCreatePkiAlertV2 = () => { return useMutation({ mutationFn: async (data) => { const { data: response } = await apiRequest.post<{ alert: TPkiAlertV2 }>( - "/api/v2/pki/alerts", + "/api/v1/cert-manager/alerts", data ); return response.alert; @@ -30,7 +30,7 @@ export const useUpdatePkiAlertV2 = () => { return useMutation({ mutationFn: async ({ alertId, ...data }) => { const { data: response } = await apiRequest.patch<{ alert: TPkiAlertV2 }>( - `/api/v2/pki/alerts/${alertId}`, + `/api/v1/cert-manager/alerts/${alertId}`, data ); return response.alert; @@ -52,7 +52,7 @@ export const useDeletePkiAlertV2 = () => { return useMutation({ mutationFn: async ({ alertId }) => { const { data } = await apiRequest.delete<{ alert: TPkiAlertV2 }>( - `/api/v2/pki/alerts/${alertId}` + `/api/v1/cert-manager/alerts/${alertId}` ); return data.alert; }, diff --git a/frontend/src/hooks/api/pkiAlertsV2/queries.ts b/frontend/src/hooks/api/pkiAlertsV2/queries.ts index d139341ae..a4ca9006f 100644 --- a/frontend/src/hooks/api/pkiAlertsV2/queries.ts +++ b/frontend/src/hooks/api/pkiAlertsV2/queries.ts @@ -24,14 +24,16 @@ export const pkiAlertsV2Keys = { }; const fetchPkiAlertsV2 = async (params: TGetPkiAlertsV2): Promise => { - const { data } = await apiRequest.get("/api/v2/pki/alerts", { + const { data } = await apiRequest.get("/api/v1/cert-manager/alerts", { params }); return data; }; const fetchPkiAlertV2ById = async ({ alertId }: TGetPkiAlertV2ById): Promise => { - const { data } = await apiRequest.get<{ alert: TPkiAlertV2 }>(`/api/v2/pki/alerts/${alertId}`); + const { data } = await apiRequest.get<{ alert: TPkiAlertV2 }>( + `/api/v1/cert-manager/alerts/${alertId}` + ); return data.alert; }; @@ -40,7 +42,7 @@ const fetchPkiAlertV2MatchingCertificates = async ( ): Promise => { const { alertId, ...queryParams } = params; const { data } = await apiRequest.get( - `/api/v2/pki/alerts/${alertId}/certificates`, + `/api/v1/cert-manager/alerts/${alertId}/certificates`, { params: queryParams } ); return data; @@ -50,7 +52,7 @@ const fetchPkiAlertV2CurrentMatchingCertificates = async ( params: TGetPkiAlertV2CurrentMatchingCertificates ): Promise => { const { data } = await apiRequest.post( - "/api/v2/pki/alerts/preview/certificates", + "/api/v1/cert-manager/alerts/preview/certificates", params ); return data; diff --git a/frontend/src/hooks/api/pkiSyncs/mutations.tsx b/frontend/src/hooks/api/pkiSyncs/mutations.tsx index 9aff79dd0..20eb1fae8 100644 --- a/frontend/src/hooks/api/pkiSyncs/mutations.tsx +++ b/frontend/src/hooks/api/pkiSyncs/mutations.tsx @@ -17,7 +17,10 @@ export const useCreatePkiSync = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ destination, ...params }: TCreatePkiSyncDTO) => { - const { data } = await apiRequest.post(`/api/v1/pki/syncs/${destination}`, params); + const { data } = await apiRequest.post( + `/api/v1/cert-manager/syncs/${destination}`, + params + ); return data; }, @@ -31,7 +34,7 @@ export const useUpdatePkiSync = () => { return useMutation({ mutationFn: async ({ syncId, projectId, destination, ...params }: TUpdatePkiSyncDTO) => { const { data } = await apiRequest.patch( - `/api/v1/pki/syncs/${destination}/${syncId}`, + `/api/v1/cert-manager/syncs/${destination}/${syncId}`, params, { params: { projectId } } ); @@ -49,9 +52,12 @@ export const useDeletePkiSync = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ syncId, projectId, destination }: TDeletePkiSyncDTO) => { - const { data } = await apiRequest.delete(`/api/v1/pki/syncs/${destination}/${syncId}`, { - params: { projectId } - }); + const { data } = await apiRequest.delete( + `/api/v1/cert-manager/syncs/${destination}/${syncId}`, + { + params: { projectId } + } + ); return data; }, @@ -66,7 +72,9 @@ export const useTriggerPkiSyncSyncCertificates = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ syncId, destination }: TTriggerPkiSyncSyncCertificatesDTO) => { - const { data } = await apiRequest.post(`/api/v1/pki/syncs/${destination}/${syncId}/sync`); + const { data } = await apiRequest.post( + `/api/v1/cert-manager/syncs/${destination}/${syncId}/sync` + ); return data; }, @@ -111,7 +119,9 @@ export const useTriggerPkiSyncImportCertificates = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ syncId, destination }: TTriggerPkiSyncImportCertificatesDTO) => { - const { data } = await apiRequest.post(`/api/v1/pki/syncs/${destination}/${syncId}/import`); + const { data } = await apiRequest.post( + `/api/v1/cert-manager/syncs/${destination}/${syncId}/import` + ); return data; }, @@ -157,7 +167,7 @@ export const useTriggerPkiSyncRemoveCertificates = () => { return useMutation({ mutationFn: async ({ syncId, destination }: TTriggerPkiSyncRemoveCertificatesDTO) => { const { data } = await apiRequest.post( - `/api/v1/pki/syncs/${destination}/${syncId}/remove-certificates` + `/api/v1/cert-manager/syncs/${destination}/${syncId}/remove-certificates` ); return data; @@ -209,9 +219,12 @@ export const useAddCertificatesToPkiSync = () => { pkiSyncId: string; certificateIds: string[]; }) => { - const { data } = await apiRequest.post(`/api/v1/pki/syncs/${pkiSyncId}/certificates`, { - certificateIds - }); + const { data } = await apiRequest.post( + `/api/v1/cert-manager/syncs/${pkiSyncId}/certificates`, + { + certificateIds + } + ); return data; }, @@ -231,9 +244,12 @@ export const useRemoveCertificatesFromPkiSync = () => { pkiSyncId: string; certificateIds: string[]; }) => { - const { data } = await apiRequest.delete(`/api/v1/pki/syncs/${pkiSyncId}/certificates`, { - data: { certificateIds } - }); + const { data } = await apiRequest.delete( + `/api/v1/cert-manager/syncs/${pkiSyncId}/certificates`, + { + data: { certificateIds } + } + ); return data; }, diff --git a/frontend/src/hooks/api/pkiSyncs/queries.tsx b/frontend/src/hooks/api/pkiSyncs/queries.tsx index 6e7aabc42..7000de99d 100644 --- a/frontend/src/hooks/api/pkiSyncs/queries.tsx +++ b/frontend/src/hooks/api/pkiSyncs/queries.tsx @@ -37,7 +37,9 @@ export const usePkiSyncOptions = ( return useQuery({ queryKey: pkiSyncKeys.options(), queryFn: async () => { - const { data } = await apiRequest.get("/api/v1/pki/syncs/options"); + const { data } = await apiRequest.get( + "/api/v1/cert-manager/syncs/options" + ); return data.pkiSyncOptions; }, @@ -58,7 +60,7 @@ export const fetchPkiSyncsByProjectId = async (projectId: string, certificateId? params.certificateId = certificateId; } - const { data } = await apiRequest.get("/api/v1/pki/syncs", { + const { data } = await apiRequest.get("/api/v1/cert-manager/syncs", { params }); @@ -110,7 +112,7 @@ export const useGetPkiSync = ( return useQuery({ queryKey: pkiSyncKeys.byId(syncId, projectId), queryFn: async () => { - const { data } = await apiRequest.get(`/api/v1/pki/syncs/${syncId}`, { + const { data } = await apiRequest.get(`/api/v1/cert-manager/syncs/${syncId}`, { params: { projectId } }); @@ -138,7 +140,7 @@ export const useListPkiSyncCertificates = ( return useQuery({ queryKey: pkiSyncKeys.certificates(syncId, { offset, limit }), queryFn: async () => { - const { data } = await apiRequest.get(`/api/v1/pki/syncs/${syncId}/certificates`, { + const { data } = await apiRequest.get(`/api/v1/cert-manager/syncs/${syncId}/certificates`, { params: { offset, limit } }); return { diff --git a/frontend/src/index.css b/frontend/src/index.css index fdedc2f68..baa613b36 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,9 +1,5 @@ @import "tailwindcss"; -@import "@fontsource/inter/400.css" layer(base); -@import "@fontsource/inter/500.css" layer(base); -@import "@fontsource/inter/700.css" layer(base); - @source not "../public"; /* diff --git a/frontend/src/layouts/KmsLayout/KmsLayout.tsx b/frontend/src/layouts/KmsLayout/KmsLayout.tsx index 9a5dc5aee..8ffe0da44 100644 --- a/frontend/src/layouts/KmsLayout/KmsLayout.tsx +++ b/frontend/src/layouts/KmsLayout/KmsLayout.tsx @@ -14,8 +14,8 @@ export const KmsLayout = () => { const location = useLocation(); return ( -
-
+
+
{ } }, [subscription, isBillingPage, isModalIntrusive]); - const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); - const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined; - const handleOrgChange = async (orgId: string) => { queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() }); @@ -249,7 +245,9 @@ export const Navbar = () => { const isServerAdminPanel = location.pathname.startsWith("/admin"); - const isProjectScope = location.pathname.startsWith(`/organizations/${currentOrg.id}/projects`); + const isProjectScope = + location.pathname.startsWith(`/organizations/${currentOrg.id}/projects`) && + location.pathname !== `/organizations/${currentOrg.id}/projects`; const handleOrgNav = async (org: Organization) => { if (currentOrg?.id === org.id) return; @@ -279,64 +277,59 @@ export const Navbar = () => { }; return ( -
-
-
+
+
+
infisical logo
-

/

+ {isServerAdminPanel ? ( - <> - - -
Server Console
- -

/

- {breadcrumbs ? ( - // scott: remove /admin as we show server console above - - ) : null} - + + +
Server Console
+ ) : ( <> -
+
+ {/* scott: the below is used to hide the top border from the org nav bar */} + {!isProjectScope && !isSubOrganization && ( +
+
+
+ )} -
- svg]:!text-org" - )} +
+ - -
- {getPlan(subscription)} -
+ + {currentOrg?.name} + + Organization + + {subscription.cardDeclined && ( { )} {isProjectScope && ( <> -

/

- {breadcrumbs ? ( - - ) : null} + + )} )}
- {subscription && subscription.slug === "starter" && !subscription.has_used_trial && ( + + {subscription && subscription.slug === "starter" && !subscription.has_used_trial ? ( + ) : ( +
+ {getPlan(subscription)} +
)} {/* eslint-disable-next-line no-nested-ternary */} {!location.pathname.startsWith("/admin") ? ( @@ -601,7 +594,7 @@ export const Navbar = () => { }} > - Invite Members + Invite Users ) : null } @@ -705,7 +698,7 @@ export const Navbar = () => { }} > }> - Invite Members + Invite Users ) : null diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx index 24f4acafe..e56c02cee 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx @@ -19,9 +19,9 @@ export const OrgNavBar = ({ isHidden }: Props) => { const variant = isRootOrganization ? "org" : "namespace"; return ( - <> +
{!isHidden && ( -
+
{ isOpen={popUp?.createOrg?.isOpen} onClose={() => handlePopUpToggle("createOrg", false)} /> - +
); }; diff --git a/frontend/src/layouts/PamLayout/PamLayout.tsx b/frontend/src/layouts/PamLayout/PamLayout.tsx index f04357a88..365d3c3eb 100644 --- a/frontend/src/layouts/PamLayout/PamLayout.tsx +++ b/frontend/src/layouts/PamLayout/PamLayout.tsx @@ -29,8 +29,8 @@ export const PamLayout = () => { return ( <> -
-
+
+
{ const location = useLocation(); return ( -
-
+
+
= { [ProjectType.SecretScanning]: "Secret Scanning" }; -export const ProjectSelect = () => { +const ProjectSelectInner = () => { const [searchProject, setSearchProject] = useState(""); const { currentProject: currentWorkspace } = useProject(); const { currentOrg } = useOrganization(); @@ -92,7 +92,11 @@ export const ProjectSelect = () => { }, [projects, projectFavorites, currentWorkspace]); return ( -
+
+ {/* scott: the below is used to hide the top border from the org nav bar */} +
+
+
{ projectId: currentWorkspace.id, orgId: currentWorkspace.orgId }} - className="group flex cursor-pointer items-center gap-x-1.5 overflow-hidden hover:text-white" + className="group flex cursor-pointer items-center gap-x-2 overflow-hidden pt-0.5 text-sm text-white" > -

- {currentWorkspace?.name} -

- - - - {currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"} - + + {currentWorkspace?.name} + + {currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"} @@ -118,7 +118,7 @@ export const ProjectSelect = () => { variant="plain" colorSchema="secondary" ariaLabel="switch-project" - className="px-2 py-1" + className="top-px px-2 py-1" > @@ -238,3 +238,14 @@ export const ProjectSelect = () => {
); }; + +export const ProjectSelect = () => { + const params = useParams({ strict: false }); + + // Return null during navigation when projectId is not available + if (!params.projectId) { + return null; + } + + return ; +}; diff --git a/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx b/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx index 162260959..0a8d3adbc 100644 --- a/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx +++ b/frontend/src/layouts/SecretManagerLayout/SecretManagerLayout.tsx @@ -39,8 +39,8 @@ export const SecretManagerLayout = () => { (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); return ( -
-
+
+
{ ); return ( -
-
+
+
{ const location = useLocation(); return ( -
-
+
+
{
diff --git a/frontend/src/pages/admin/ResourceOverviewPage/ResourceOverviewPage.tsx b/frontend/src/pages/admin/ResourceOverviewPage/ResourceOverviewPage.tsx index eab91ef04..199fcb1bd 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/ResourceOverviewPage.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/ResourceOverviewPage.tsx @@ -29,7 +29,7 @@ export const ResourceOverviewPage = () => { Users - Identities + Machine Identities diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx index d17786712..0347fbcf9 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/MachineIdentitiesTable.tsx @@ -90,7 +90,7 @@ const IdentityPanelTable = ({ value={searchIdentityFilter} onChange={(e) => setSearchIdentityFilter(e.target.value)} leftIcon={} - placeholder="Search identities by name..." + placeholder="Search machine identities by name..." className="flex-1" />
diff --git a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx index 93d0253ff..0a619889b 100644 --- a/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx +++ b/frontend/src/pages/admin/ResourceOverviewPage/components/OrganizationsTable.tsx @@ -350,8 +350,8 @@ const ViewMembersModalContent = ({ className="my-auto bg-mineshaft-700" title={ members.length - ? "No organization members match search..." - : "No organization members found" + ? "No organization users match search..." + : "No organization users found" } icon={faUsers} /> diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx index 51cc9edf5..b2cd5e696 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/CertAuthDetailsByIDPage.tsx @@ -44,10 +44,9 @@ const Page = () => { const params = useParams({ from: ROUTE_PATHS.CertManager.CertAuthDetailsByIDPage.id }); - const { caName } = params as { caName: string }; + const { caId } = params as { caId: string }; const { data } = useGetCa({ - caName, - projectId: currentProject?.id || "", + caId, type: CaType.INTERNAL }) as { data: TInternalCertificateAuthority }; @@ -66,7 +65,7 @@ const Page = () => { if (!currentProject?.slug) return; await deleteCa({ - caName, + id: data.id, projectId: currentProject.id, type: CaType.INTERNAL }); @@ -138,7 +137,7 @@ const Page = () => {
- +
diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/CaCrlsTable.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/CaCrlsTable.tsx index dfe87d027..77b96f3d6 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/CaCrlsTable.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaCrlsSection/CaCrlsTable.tsx @@ -51,7 +51,7 @@ export const CaCrlsTable = ({ caId }: Props) => {
- {`${window.origin}/api/v1/pki/crl/${id}`} + {`${window.origin}/api/v1/cert-manager/crl/${id}`}
{/* {format(new Date(caCrlObj.thisUpdate), "yyyy-MM-dd")} */} diff --git a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx index dd212eb40..df33f51c8 100644 --- a/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx +++ b/frontend/src/pages/cert-manager/CertAuthDetailsByIDPage/components/CaDetailsSection.tsx @@ -4,7 +4,7 @@ import { format } from "date-fns"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, IconButton, Tooltip } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useTimedReset } from "@app/hooks"; import { CaStatus, CaType, InternalCaType, useGetCa } from "@app/hooks/api"; import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants"; @@ -13,15 +13,14 @@ import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { - caName: string; + caId: string; handlePopUpOpen: ( popUpName: keyof UsePopUpState<["ca", "renewCa", "installCaCert"]>, data?: object ) => void; }; -export const CaDetailsSection = ({ caName, handlePopUpOpen }: Props) => { - const { currentProject } = useProject(); +export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ initialState: "Copy ID to clipboard" }); @@ -30,8 +29,7 @@ export const CaDetailsSection = ({ caName, handlePopUpOpen }: Props) => { }); const { data } = useGetCa({ - caName, - projectId: currentProject.id, + caId, type: CaType.INTERNAL }); @@ -53,7 +51,7 @@ export const CaDetailsSection = ({ caName, handlePopUpOpen }: Props) => { onClick={(e) => { e.stopPropagation(); handlePopUpOpen("ca", { - name: ca.name + caId: ca.id }); }} > @@ -154,10 +152,6 @@ export const CaDetailsSection = ({ caName, handlePopUpOpen }: Props) => { : "-"}

-
-

Enable Direct Issuance

-

{ca.enableDirectIssuance ? "True" : "False"}

-
{ca.status === CaStatus.ACTIVE && ( { @@ -21,7 +21,7 @@ export const Route = createFileRoute( }) }, { - label: params.caName + label: params.caId } ] }; diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx index f80e21212..f33b8c43f 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaInstallCertModal/InternalCaInstallForm.tsx @@ -9,8 +9,8 @@ import { Button, FormControl, Input, Select, SelectItem } from "@app/components/ import { useProject } from "@app/context"; import { CaStatus, - useGetCaById, useGetCaCsr, + useGetInternalCaById, useImportCaCertificate, useListWorkspaceCas, useSignIntermediate @@ -51,7 +51,7 @@ export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { projectId: currentProject.id, status: CaStatus.ACTIVE }); - const { data: ca } = useGetCaById(caId); + const { data: ca } = useGetInternalCaById(caId); const { data: csr } = useGetCaCsr(caId); const { mutateAsync: signIntermediate } = useSignIntermediate(); @@ -83,18 +83,21 @@ export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { const parentCaId = watch("parentCaId"); - const { data: parentCa } = useGetCaById(parentCaId); + const { data: parentCa } = useGetInternalCaById(parentCaId); useEffect(() => { - if (parentCa?.maxPathLength) { + if (parentCa?.configuration.maxPathLength) { setValue( "maxPathLength", - (parentCa.maxPathLength === -1 ? 3 : parentCa.maxPathLength - 1).toString() + (parentCa.configuration.maxPathLength === -1 + ? 3 + : parentCa.configuration.maxPathLength - 1 + ).toString() ); } - if (parentCa?.notAfter) { - const parentCaNotAfter = new Date(parentCa.notAfter); + if (parentCa?.configuration.notAfter) { + const parentCaNotAfter = new Date(parentCa.configuration.notAfter); const middleDate = getMiddleDate(new Date(), parentCaNotAfter); setValue("notAfter", format(middleDate, "yyyy-MM-dd")); } @@ -197,7 +200,7 @@ export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => { onValueChange={onChange} className="w-full" > - {generatePathLengthOpts(parentCa?.maxPathLength || 0).map((value) => ( + {generatePathLengthOpts(parentCa?.configuration.maxPathLength || 0).map((value) => ( {`${value}`} diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx index 53434b79d..35be69d00 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx @@ -46,7 +46,6 @@ const schema = z name: slugSchema({ field: "Name" }), - enableDirectIssuance: z.boolean(), status: z.nativeEnum(CaStatus), configuration: z .object({ @@ -80,8 +79,7 @@ const caTypes = [ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { const { currentProject } = useProject(); const { data: ca } = useGetCa({ - caName: (popUp?.ca?.data as { name: string })?.name || "", - projectId: currentProject?.id || "", + caId: (popUp?.ca?.data as { caId: string })?.caId || "", type: CaType.INTERNAL }); @@ -100,7 +98,6 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { type: CaType.INTERNAL, name: "", status: CaStatus.ACTIVE, - enableDirectIssuance: true, configuration: { type: InternalCaType.ROOT, organization: "", @@ -124,7 +121,6 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { type: ca.type, name: ca.name, status: ca.status, - enableDirectIssuance: ca.enableDirectIssuance, configuration: { type: ca.configuration.type, organization: ca.configuration.organization, @@ -151,7 +147,6 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { type: CaType.INTERNAL, name: "", status: CaStatus.ACTIVE, - enableDirectIssuance: false, configuration: { type: InternalCaType.ROOT, organization: "", @@ -168,24 +163,16 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { } }, [ca]); - const onFormSubmit = async ({ - type, - name, - enableDirectIssuance, - status, - configuration - }: FormData) => { + const onFormSubmit = async ({ type, name, status, configuration }: FormData) => { if (!currentProject?.slug) return; if (ca) { // update await updateMutateAsync({ - caName: ca.name, - projectId: currentProject.id, + id: ca.id, name, type: CaType.INTERNAL, - status, - enableDirectIssuance + status }); } else { // create @@ -194,7 +181,6 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { name, type, status, - enableDirectIssuance, configuration: { ...configuration, maxPathLength: Number(configuration.maxPathLength) diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx index 918a844b1..7e121eec9 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaSection.tsx @@ -26,10 +26,10 @@ export const CaSection = () => { "caStatus" // enable / disable ] as const); - const onRemoveCaSubmit = async (caName: string) => { + const onRemoveCaSubmit = async (id: string) => { if (!currentProject?.slug) return; - await deleteCa({ caName, projectId: currentProject.id, type: CaType.INTERNAL }); + await deleteCa({ id, projectId: currentProject.id, type: CaType.INTERNAL }); createNotification({ text: "Successfully deleted CA", @@ -39,10 +39,10 @@ export const CaSection = () => { handlePopUpClose("deleteCa"); }; - const onUpdateCaStatus = async ({ caName, status }: { caName: string; status: CaStatus }) => { + const onUpdateCaStatus = async ({ caId, status }: { caId: string; status: CaStatus }) => { if (!currentProject?.slug) return; - await updateCa({ caName, projectId: currentProject.id, type: CaType.INTERNAL, status }); + await updateCa({ id: caId, type: CaType.INTERNAL, status }); createNotification({ text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`, @@ -85,9 +85,7 @@ export const CaSection = () => { subTitle="This action will delete other CAs and certificates below it in your CA hierarchy." onChange={(isOpen) => handlePopUpToggle("deleteCa", isOpen)} deleteKey="confirm" - onDeleteApproved={() => - onRemoveCaSubmit((popUp?.deleteCa?.data as { caName: string })?.caName) - } + onDeleteApproved={() => onRemoveCaSubmit((popUp?.deleteCa?.data as { caId: string })?.caId)} /> { onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)} deleteKey="confirm" onDeleteApproved={() => - onUpdateCaStatus(popUp?.caStatus?.data as { caName: string; status: CaStatus }) + onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus }) } />
diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx index 2549cf327..27de9315a 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaTable.tsx @@ -81,11 +81,11 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { key={`ca-${ca.id}`} onClick={() => navigate({ - to: "/organizations/$orgId/projects/cert-management/$projectId/ca/$caName", + to: "/organizations/$orgId/projects/cert-management/$projectId/ca/$caId", params: { orgId: currentOrg.id, projectId: currentProject.id, - caName: ca.name + caId: ca.id } }) } @@ -180,7 +180,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { onClick={(e) => { e.stopPropagation(); handlePopUpOpen("caStatus", { - caName: ca.name, + caId: ca.id, status: ca.status === CaStatus.ACTIVE ? CaStatus.DISABLED @@ -207,7 +207,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { onClick={(e) => { e.stopPropagation(); handlePopUpOpen("deleteCa", { - caName: ca.name + caId: ca.id }); }} disabled={!isAllowed} diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx index 1fb338a25..9d7fd6781 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx @@ -14,8 +14,7 @@ import { Modal, ModalContent, Select, - SelectItem, - Switch + SelectItem } from "@app/components/v2"; import { useProject } from "@app/context"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; @@ -27,6 +26,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, @@ -59,7 +62,6 @@ const baseSchema = z.object({ name: slugSchema({ field: "Name" }), - enableDirectIssuance: z.boolean(), status: z.nativeEnum(CaStatus) }); @@ -131,8 +133,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { const { currentProject } = useProject(); const { data: ca, isLoading: isCaLoading } = useGetCa({ - caName: (popUp?.ca?.data as { name: string })?.name || "", - projectId: currentProject?.id || "", + caId: (popUp?.ca?.data as { caId: string })?.caId || "", type: (popUp?.ca?.data as { type: CaType })?.type || "" }); @@ -168,7 +169,6 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { type: CaType.AZURE_AD_CS, name: "", status: CaStatus.ACTIVE, - enableDirectIssuance: false, configuration: { azureAdcsConnection: { id: "", @@ -181,7 +181,6 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { type: CaType.ACME, name: "", status: CaStatus.ACTIVE, - enableDirectIssuance: true, configuration: { dnsAppConnection: { id: "", @@ -211,6 +210,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 @@ -220,16 +224,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 @@ -241,6 +253,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) { @@ -253,7 +270,6 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { type: ca.type, name: ca.name, status: ca.status, - enableDirectIssuance: ca.enableDirectIssuance, configuration: { dnsAppConnection: { id: ca.configuration.dnsAppConnectionId, @@ -278,7 +294,6 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { type: ca.type, name: ca.name, status: ca.status, - enableDirectIssuance: false, configuration: { azureAdcsConnection: { id: ca.configuration.azureAdcsConnectionId, @@ -293,7 +308,6 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ type, name, - enableDirectIssuance, status, configuration: formConfiguration }: FormData) => { @@ -320,12 +334,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { if (ca) { await updateMutateAsync({ - caName: ca.name, + id: ca.id, projectId: currentProject.id, name, type, status, - enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance, configuration: configPayload }); } else { @@ -334,7 +347,6 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { name, type, status, - enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance, configuration: configPayload }); } @@ -500,6 +512,32 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> )} + {dnsProvider === AcmeDnsProvider.DNSMadeEasy && ( + ( + + zone.id === value)} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + }} + options={dnsMadeEasyZones} + placeholder="Select a zone..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> + + )} + /> + )} { name="configuration.azureAdcsConnection" /> )} - {caType === CaType.ACME && ( - { - return ( - - field.onChange(value)} - isChecked={field.value} - > -

Enable Direct Issuance

-
-
- ); - }} - /> - )}
diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx index 14e8d100a..34b8a80cc 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal.tsx @@ -50,11 +50,11 @@ export const CertificateRevocationModal = ({ popUp, handlePopUpToggle }: Props) const onFormSubmit = async ({ revocationReason }: FormData) => { if (!currentProject?.slug) return; - const { serialNumber } = popUp.revokeCertificate.data as { serialNumber: string }; + const { certificateId } = popUp.revokeCertificate.data as { certificateId: string }; await revokeCertificate({ projectId: currentProject.id, - serialNumber, + id: certificateId, revocationReason }); diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx index 0ba3721e3..c20678724 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateTemplateModal.tsx @@ -26,8 +26,8 @@ import { useProject } from "@app/context"; import { CaStatus, useCreateCertTemplate, - useGetCaById, useGetCertTemplate, + useGetInternalCaById, useListWorkspaceCas, useListWorkspacePkiCollections, useUpdateCertTemplate @@ -84,7 +84,7 @@ type Props = { export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Props) => { const { currentProject } = useProject(); - const { data: ca } = useGetCaById(caId); + const { data: ca } = useGetInternalCaById(caId); const { data: certTemplate } = useGetCertTemplate( (popUp?.certificateTemplate?.data as { id: string })?.id || "" diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx index 469df62e5..e476f429d 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesSection.tsx @@ -39,11 +39,11 @@ export const CertificatesSection = () => { "managePkiSyncs" ] as const); - const onRemoveCertificateSubmit = async (serialNumber: string) => { + const onRemoveCertificateSubmit = async (id: string) => { if (!currentProject?.slug) return; await deleteCert({ - serialNumber, + id, projectId: currentProject.id }); @@ -57,7 +57,13 @@ export const CertificatesSection = () => { const handleCertificateExport = async ( format: "pem" | "pkcs12", - serialNumber: string, + { + certificateId, + serialNumber + }: { + certificateId: string; + serialNumber: string; + }, options?: ExportOptions ) => { if (format === "pem") { @@ -75,7 +81,7 @@ export const CertificatesSection = () => { try { await downloadCertPkcs12({ - serialNumber, + certificateId, projectSlug: currentProject.slug, password: options.pkcs12.password, alias: options.pkcs12.alias @@ -150,7 +156,7 @@ export const CertificatesSection = () => { deleteKey="confirm" onDeleteApproved={() => onRemoveCertificateSubmit( - (popUp?.deleteCertificate?.data as { serialNumber: string })?.serialNumber + (popUp?.deleteCertificate?.data as { certificateId: string })?.certificateId ) } /> diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx index 39b43129c..b4defeb3a 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx @@ -277,6 +277,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} onClick={async () => handlePopUpOpen("certificateExport", { + certificateId: certificate.id, serialNumber: certificate.serialNumber }) } @@ -501,7 +502,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} onClick={async () => handlePopUpOpen("revokeCertificate", { - serialNumber: certificate.serialNumber + certificateId: certificate.id }) } disabled={!isAllowed} @@ -524,7 +525,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { )} onClick={async () => handlePopUpOpen("deleteCertificate", { - serialNumber: certificate.serialNumber, + certificateId: certificate.id, commonName: certificate.commonName }) } diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/IntegrationsListPage.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/IntegrationsListPage.tsx index 9387074eb..8cf23c725 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/IntegrationsListPage.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/IntegrationsListPage.tsx @@ -49,7 +49,7 @@ export const IntegrationsListPage = () => {
diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx index 129af29d3..5540ab4d6 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/components/PkiSubscriberCertificatesTable.tsx @@ -171,7 +171,7 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen onClick={() => handlePopUpOpen && handlePopUpOpen("revokeCertificate", { - serialNumber: certificate.serialNumber + certificateId: certificate.id }) } disabled={!isAllowed} diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index edb16f519..e7530027b 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -675,7 +675,7 @@ export const CreateProfileModal = ({ isDisabled={Boolean(isEdit)} > Certificate Authority - Self-signed + Self-Signed )} diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx index 1373c9fed..b719c58aa 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx @@ -30,7 +30,7 @@ import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; import { usePopUp, useToggle } from "@app/hooks"; -import { useGetCaById } from "@app/hooks/api/ca/queries"; +import { useGetInternalCaById } from "@app/hooks/api/ca/queries"; import { IssuerType, TCertificateProfile } from "@app/hooks/api/certificateProfiles"; import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries"; import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal"; @@ -50,9 +50,7 @@ export const ProfileRow = ({ }: Props) => { const { permission } = useProjectPermission(); - const { data: caData } = useGetCaById( - profile.certificateAuthority?.isExternal ? "" : (profile.caId ?? "") - ); + const { data: caData } = useGetInternalCaById(profile.caId ?? ""); const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const); @@ -123,18 +121,15 @@ export const ProfileRow = ({ {getEnrollmentTypeBadge(profile.enrollmentType)} -
- - {profile.issuerType === IssuerType.SELF_SIGNED - ? "Self-signed" - : profile.certificateAuthority?.isExternal - ? profile.certificateAuthority.name - : caData?.friendlyName || - caData?.commonName || - profile.certificateAuthority?.name || - profile.caId} - -
+ + {profile.issuerType === IssuerType.SELF_SIGNED + ? "Self-signed" + : profile.certificateAuthority?.isExternal + ? profile.certificateAuthority.name + : caData?.configuration.friendlyName || + caData?.configuration.commonName || + profile.caId} + diff --git a/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx b/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx index b75ab2afc..81eb4e170 100644 --- a/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx @@ -1,7 +1,10 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; +import { Link } from "@tanstack/react-router"; +import { InfoIcon } from "lucide-react"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { useOrganization } from "@app/context"; import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab"; @@ -15,6 +18,7 @@ const tabs = [ export const SettingsPage = () => { const { t } = useTranslation(); + const { currentOrg } = useOrganization(); return (
@@ -22,7 +26,17 @@ export const SettingsPage = () => { {t("common.head-title", { title: t("settings.project.title") })}
- + + + Looking for organization settings? + + {tabs.map((tab) => ( diff --git a/frontend/src/pages/kms/OverviewPage/OverviewPage.tsx b/frontend/src/pages/kms/OverviewPage/OverviewPage.tsx index be3be8286..3069772a1 100644 --- a/frontend/src/pages/kms/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/kms/OverviewPage/OverviewPage.tsx @@ -20,7 +20,7 @@ export const OverviewPage = () => {
{ const { t } = useTranslation(); + const { currentOrg } = useOrganization(); + return (
@@ -24,9 +29,19 @@ export const SettingsPage = () => {
+ > + + Looking for organization settings? + + {tabs.map((tab) => ( diff --git a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx index d3e93bcea..0d5456f6d 100644 --- a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx @@ -58,7 +58,7 @@ export const AccessManagementPage = () => { }, { key: OrgAccessControlTabSections.Identities, - label: "Identities", + label: "Machine Identities", isHidden: permission.cannot( OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity @@ -83,8 +83,8 @@ export const AccessManagementPage = () => {
{!currentOrg.shouldUseNewPrivilegeSystem && (
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx index a5e2e113c..2f66c6644 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -51,19 +51,19 @@ export const OrgGroupsSection = () => {
-

Groups

+

Organization Groups

{(isAllowed) => ( )} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx index 5b4698ec9..e0cd09d83 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -159,7 +159,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { value={search} onChange={(e) => setSearch(e.target.value)} leftIcon={} - placeholder="Search groups..." + placeholder="Search organization groups..." /> @@ -205,7 +205,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
- Role + Organization Role
-

Identities

+

+ Organization Machine Identities +

@@ -116,7 +118,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 +131,9 @@ export const IdentitySection = withPermission( }} isDisabled={!isAllowed} > - Create Identity + {isSubOrganization + ? "Add Machine Identity to Sub-Organization" + : "Create Organization Machine Identity"} )} @@ -141,7 +145,9 @@ export const IdentitySection = withPermission(
-

Identity Auth Templates

+

+ Machine Identity Auth Templates +

{(isAllowed) => (
-
Assign Existing Identity
+
Assign Existing Machine Identity
- 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.
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index 0c8cfd6bb..b94846f07 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -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) => { { - Apply Roles to Filter Identities + Filter Organization Machine Identities by Role {roles?.map(({ id, slug, name }) => ( { value={search} onChange={(e) => setSearch(e.target.value)} leftIcon={} - placeholder="Search identities by name..." + placeholder="Search machine identities by name..." />
@@ -258,7 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
- Role + Organization Role { }} isDisabled={!isAllowed} > - Edit Identity {isSubOrgIdentity ? "" : "Membership"} + Edit Machine Identity {isSubOrgIdentity ? "" : "Membership"} )} @@ -428,7 +428,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { icon={} > {isSubOrgIdentity - ? "Delete Identity" + ? "Delete Machine Identity" : "Remove From Sub-Organization"} )} @@ -455,8 +455,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { 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} /> diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityLinkForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityLinkForm.tsx index dde0588de..9f7141802 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityLinkForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityLinkForm.tsx @@ -78,11 +78,11 @@ export const OrgIdentityLinkForm = ({ onClose }: Props) => { control={control} name="identity" render={({ field: { onChange, value }, fieldState: { error } }) => ( - + option.id} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityModal.tsx index dfc496ca8..737c454d1 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/OrgIdentityModal.tsx @@ -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" }); @@ -255,9 +255,7 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => { />
- {i === 0 && ( - - )} + {i === 0 && } 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" }); } diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index 10b0cd07d..6a4d93141 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -205,13 +205,13 @@ export const OrgMembersSection = () => {
-

Users

+

Organization Users

{(isAllowed) => ( )} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index d81c0a3eb..55b593ffe 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -336,7 +336,7 @@ export const OrgMembersTable = ({ - Apply Roles to Filter Users + Filter Organization Users by Role {roles?.map(({ id, slug, name }) => ( setSearch(e.target.value)} leftIcon={} - placeholder="Search members..." + placeholder="Search organization users..." />
@@ -434,7 +434,7 @@ export const OrgMembersTable = ({
- Role + Organization Role diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index ee3c536e8..63b4284f1 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -199,7 +199,7 @@ export const OrgRoleTable = () => { {(isAllowed) => ( )} @@ -216,7 +216,7 @@ export const OrgRoleTable = () => { value={search} onChange={(e) => setSearch(e.target.value)} leftIcon={} - placeholder="Search roles..." + placeholder="Search organization roles..." className="flex-1" containerClassName="mb-4" /> @@ -441,7 +441,7 @@ export const OrgRoleTable = () => {
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index aca33ffa2..af26a940d 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -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 ; case AppConnection.Cloudflare: return ; + case AppConnection.DNSMadeEasy: + return ; case AppConnection.Bitbucket: return ; case AppConnection.Zabbix: @@ -306,6 +309,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { ); case AppConnection.Cloudflare: return ; + case AppConnection.DNSMadeEasy: + return ; case AppConnection.Bitbucket: return ; case AppConnection.Zabbix: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DNSMadeEasyConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DNSMadeEasyConnectionForm.tsx new file mode 100644 index 000000000..9d3c23743 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/DNSMadeEasyConnectionForm.tsx @@ -0,0 +1,157 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +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; +}; + +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; + +export const DNSMadeEasyConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.DNSMadeEasy, + method: DNSMadeEasyConnectionMethod.APIKeySecret, + credentials: { + apiKey: "", + secretKey: "" + } + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + onChange(e.target.value)} + placeholder="af1b628f-3272-46aa-9cde-837d0c59155d" + /> + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx b/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx index 37d73125c..5f2290ab3 100644 --- a/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/AuditLogsPage.tsx @@ -19,7 +19,7 @@ export const AuditLogsPage = () => {
diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx index 055adfedc..8c606f07c 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx @@ -88,7 +88,7 @@ const Page = () => { className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" > - Groups + Organization Groups { }); 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" > - Identities + Organization Machine Identities
@@ -111,7 +111,7 @@ const Page = () => { }) } > - Unlink Identity + Unlink Machine Identity )} @@ -142,7 +142,7 @@ const Page = () => { > diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx index b19901bef..f9bbdce9d 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx @@ -45,7 +45,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdent return data ? (
-

Identity Details

+

Details

-

Identity ID

+

Machine Identity ID

{data.identity.id}

diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx index 5c4dcd6dd..fb11c758e 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityAddToProjectModal.tsx @@ -167,7 +167,7 @@ export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle handlePopUpToggle("addIdentityToProject", isOpen); }} > - + diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx index 4dcc1996e..daf40c21b 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx @@ -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} /> diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx index 4942bf2e2..8ea1807cb 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx @@ -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 ? ( <> { ) } 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 = () => { ) : ( <> setIdentityToken(e.target.value)} - placeholder="Enter identity token..." + placeholder="Enter machine identity token..." isError={Boolean(errors.identityToken)} /> {errors.identityToken &&

{errors.identityToken}

} @@ -325,15 +325,15 @@ export const GatewayCliDeploymentMethod = () => { className="mr-2" >
- Automatically enable token auth and generate a token for identity + Automatically enable token auth and generate a token for machine identity - 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.

A token will automatically be generated to be used with the CLI command. diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliSystemdDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliSystemdDeploymentMethod.tsx index 860590ffa..1603f3bdd 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliSystemdDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliSystemdDeploymentMethod.tsx @@ -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 ? ( <> { ) } 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 = () => { ) : ( <> setIdentityToken(e.target.value)} - placeholder="Enter identity token..." + placeholder="Enter machine identity token..." isError={Boolean(errors.identityToken)} /> {errors.identityToken &&

{errors.identityToken}

} @@ -347,15 +347,15 @@ export const GatewayCliSystemdDeploymentMethod = () => { className="mr-2" >
- Automatically enable token auth and generate a token for identity + Automatically enable token auth and generate a token for machine identity - 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.

A token will automatically be generated to be used with the CLI command. diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx index 87f2ed75b..ce8e988c0 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx @@ -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 ? ( <> { ) } 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 = () => { ) : ( <> setIdentityToken(e.target.value)} - placeholder="Enter identity token..." + placeholder="Enter machine identity token..." isError={Boolean(errors.identityToken)} /> {errors.identityToken &&

{errors.identityToken}

} @@ -279,15 +279,15 @@ export const RelayCliDeploymentMethod = () => { className="mr-2" >
- Automatically enable token auth and generate a token for identity + Automatically enable token auth and generate a token for machine identity - 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.

A token will automatically be generated to be used with the CLI command. diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliSystemdDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliSystemdDeploymentMethod.tsx index 10e4d9edd..635e07eba 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliSystemdDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliSystemdDeploymentMethod.tsx @@ -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 ? ( <> { ) } 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 = () => { ) : ( <> setIdentityToken(e.target.value)} - placeholder="Enter identity token..." + placeholder="Enter machine identity token..." isError={Boolean(errors.identityToken)} /> {errors.identityToken &&

{errors.identityToken}

} @@ -320,15 +320,15 @@ export const RelayCliSystemdDeploymentMethod = () => { className="mr-2" >
- Automatically enable token auth and generate a token for identity + Automatically enable token auth and generate a token for machine identity - 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.

A token will automatically be generated to be used with the CLI command. diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx index d9c67d610..301930741 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayTerraformDeploymentMethod.tsx @@ -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 ? ( <> membership.identity)} getOptionValue={(option) => option.id} getOptionLabel={(option) => option.name} @@ -374,14 +374,14 @@ resource "aws_eip_association" "eip_assoc" { ) : ( <> setIdentityToken(e.target.value)} - placeholder="Enter identity token..." + placeholder="Enter machine identity token..." isError={Boolean(errors.identityToken)} /> {errors.identityToken &&

{errors.identityToken}

} @@ -399,15 +399,15 @@ resource "aws_eip_association" "eip_assoc" { className="mr-2" >
- Automatically enable token auth and generate a token for identity + Automatically enable token auth and generate a token for machine identity - 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.

A token will automatically be generated to be used with the CLI command. diff --git a/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx b/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx index 6e1bc1398..c0117b758 100644 --- a/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx @@ -81,7 +81,7 @@ export const ProjectsPage = () => { {projectListView === ProjectListView.MyProjects ? ( diff --git a/frontend/src/pages/organization/SettingsPage/SettingsPage.tsx b/frontend/src/pages/organization/SettingsPage/SettingsPage.tsx index b3a7c1043..cec33b289 100644 --- a/frontend/src/pages/organization/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/organization/SettingsPage/SettingsPage.tsx @@ -20,7 +20,7 @@ export const SettingsPage = () => {
diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx index 3b26f80f0..608348029 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx @@ -112,7 +112,7 @@ const Page = withPermission( className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" > - Users + Organization Users { const { t } = useTranslation(); + const { currentOrg } = useOrganization(); + return (
@@ -16,9 +21,19 @@ export const SettingsPage = () => {
+ > + + Looking for organization settings? + + diff --git a/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx b/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx index 84a5376ab..83624c98d 100644 --- a/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx +++ b/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx @@ -1,6 +1,7 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; -import { useNavigate, useSearch } from "@tanstack/react-router"; +import { Link, useNavigate, useSearch } from "@tanstack/react-router"; +import { InfoIcon } from "lucide-react"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useOrganization, useProject } from "@app/context"; @@ -43,9 +44,19 @@ const Page = () => {
+ title="Project Access Control" + description="Manage fine-grained access for users, groups, roles, and machine identities within your project resources." + > + + Looking for organization access control? + + @@ -55,7 +66,7 @@ const Page = () => { Groups - Identities + Machine Identities {isSecretManager && ( diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx index 8415a5e88..b6c0574cc 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx @@ -59,19 +59,19 @@ export const GroupsSection = () => {
-

User Groups

+

Project Groups

{(isAllowed) => ( )} diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx index c3d850c6b..fd8f5fe3c 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx @@ -122,7 +122,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { value={search} onChange={(e) => setSearch(e.target.value)} leftIcon={} - placeholder="Search members..." + placeholder="Search project groups..." /> @@ -143,7 +143,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { - + diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx index 72bae83f2..2fd8bfb62 100644 --- a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx @@ -12,8 +12,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate } from "@tanstack/react-router"; -import { AnimatePresence, motion } from "framer-motion"; -import { LinkIcon, PlusIcon } from "lucide-react"; +import { InfoIcon } from "lucide-react"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; @@ -83,10 +82,9 @@ import { ProjectLinkIdentityModal } from "./components/ProjectLinkIdentityModal" const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; -enum WizardSteps { - SelectAction = "select-action", - LinkIdentity = "link-identity", - ProjectIdentity = "project-identity" +enum AddIdentityType { + CreateNew, + AssignExisting } export const IdentityTab = withProjectPermission( @@ -95,7 +93,9 @@ export const IdentityTab = withProjectPermission( const navigate = useNavigate(); const { isSubOrganization, currentOrg } = useOrganization(); - const [wizardStep, setWizardStep] = useState(WizardSteps.SelectAction); + const [addMachineIdentityType, setAddMachineIdentityType] = useState( + AddIdentityType.CreateNew + ); const { offset, @@ -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(
-

Identities

+

Project Machine Identities

@@ -212,7 +212,7 @@ export const IdentityTab = withProjectPermission( onClick={() => handlePopUpOpen("createIdentity")} isDisabled={!isAllowed} > - Create Identity + Add Machine Identity to Project )} @@ -223,7 +223,7 @@ export const IdentityTab = withProjectPermission( value={search} onChange={(e) => setSearch(e.target.value)} leftIcon={} - placeholder="Search identities by name..." + placeholder="Search project machine identities by name..." />
RoleProject Role Added on
@@ -251,7 +251,7 @@ export const IdentityTab = withProjectPermission( - + @@ -454,7 +454,9 @@ export const IdentityTab = withProjectPermission( }); }} > - {identityProjectId ? "Delete Identity" : "Remove From Project"} + {identityProjectId + ? "Delete Machine Identity" + : "Remove From Project"} )} @@ -474,7 +476,7 @@ export const IdentityTab = withProjectPermission( @@ -497,8 +499,8 @@ export const IdentityTab = withProjectPermission( 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} /> @@ -508,93 +510,89 @@ export const IdentityTab = withProjectPermission( isOpen={popUp.createIdentity.isOpen} onOpenChange={(open) => { handlePopUpToggle("createIdentity", open); - if (!open) setWizardStep(WizardSteps.SelectAction); }} > - - {wizardStep === WizardSteps.SelectAction && ( - +
+ +
+ +

+ You can add machine identities to your project in one of two ways: +

+
    +
  • + Create New - + Create a dedicated machine identity managed at the project-level. +

    + This method is recommended for autonomous teams that need to manage + machine identity authentication. +

    +
  • +
  • + Assign Existing{" "} + - Assign an existing machine identity from your organization. +

    + This method is recommended for organizations that need to maintain + centralized control. +

    +
  • +
+ + } + > + +
+ + {addMachineIdentityType === AddIdentityType.CreateNew && ( + { + handlePopUpClose("createIdentity"); + }} + /> + )} + {addMachineIdentityType === AddIdentityType.AssignExisting && ( + + )}
{ } 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, @@ -233,9 +233,7 @@ export const ProjectIdentityModal = ({ onClose, identity }: ContentProps) => { />
- {i === 0 && ( - - )} + {i === 0 && } { }); createNotification({ - text: "Successfully added identity to project", + text: "Successfully added machine identity to project", type: "success" }); @@ -101,24 +101,18 @@ export const ProjectLinkIdentityModal = ({ handlePopUpToggle }: Props) => { handlePopUpToggle("createIdentity", false); }; - if (isMembershipsLoading || isRolesLoading) - return ( -
- -
- ); - return (
( - + ({ name: membership.name, @@ -142,6 +136,7 @@ export const ProjectLinkIdentityModal = ({ handlePopUpToggle }: Props) => { > { isLoading={isSubmitting} isDisabled={isSubmitting} > - Link + Assign to Project )} diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx index d554f12fb..df2983655 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx @@ -208,7 +208,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => { - Apply Roles to Filter Users + Filter Project Users by Role {projectRoles?.map(({ id, slug, name }) => ( { value={search} onChange={(e) => setSearch(e.target.value)} leftIcon={} - placeholder="Search members..." + placeholder="Search project users..." />
@@ -282,7 +282,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => { -
+ @@ -462,9 +462,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => { )} {!isMembersLoading && !filteredUsers?.length && ( )} diff --git a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index eeb29725f..cd76677f0 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -170,13 +170,13 @@ export const ProjectRoleList = () => { {(isAllowed) => ( )} diff --git a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx index 6f2e82e02..5c019c5be 100644 --- a/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/ServiceTokenTab/components/ServiceTokenSection/ServiceTokenSection.tsx @@ -57,7 +57,7 @@ export const ServiceTokenSection = withProjectPermission( > {(isAllowed) => ( )} @@ -219,7 +219,7 @@ const Page = () => { {!isProjectIdentity && ( - This identity is managed by your organization.{" "} + This machine identity is managed by your organization.{" "} { }} > - Click here to manage identity. + Click here to manage machine identity. ) : null @@ -286,15 +286,15 @@ const Page = () => { handlePopUpToggle("assumePrivileges", isOpen)} onConfirmed={handleAssumePrivileges} buttonText="Confirm" /> ) : ( - + )} ); diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx index b11bd9dae..e43b80538 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx @@ -235,7 +235,10 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
RoleProject Role Managed by {isFetching ? : null}
RoleProject Role
{!isPending && !identityProjectPrivileges?.length && ( - + )}
diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx index dae996489..e4ad97f9d 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx @@ -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 (
-

Identity Details

+

Details

{!isOrgIdentity && ( @@ -114,7 +114,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members }} disabled={!isAllowed} > - Edit Identity + Edit Machine Identity )} @@ -137,7 +137,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members icon={} disabled={!isAllowed} > - Delete Identity + Delete Machine Identity )} @@ -146,7 +146,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
-

Identity ID

+

Machine Identity ID

{identity.id}

diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx index cfaf9933c..62b4e4c43 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx @@ -128,7 +128,7 @@ export const Page = () => { className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" > - Users + Project Users { className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" > - Roles + Project Roles }; const Content = ({ onClose, type: projectType }: ContentProps) => { const rootForm = useFormContext(); const [search, setSearch] = useState(""); + const { currentProject } = useProject(); + const { data: integrations = [] } = useGetWorkspaceIntegrations(currentProject?.id ?? ""); + const { control, handleSubmit, @@ -60,6 +64,8 @@ const Content = ({ onClose, type: projectType }: ContentProps) => { } }); + const hasNativeIntegrations = integrations.length > 0; + const filteredPolicies = Object.entries(PROJECT_PERMISSION_OBJECT) .filter( ([subject, { title }]) => @@ -68,6 +74,11 @@ const Content = ({ onClose, type: projectType }: ContentProps) => { ] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true) ) .filter(([subject]) => !EXCLUDED_PERMISSION_SUBS.includes(subject as ProjectPermissionSub)) + .filter( + ([subject]) => + // Hide Native Integrations policy if project has no integrations + subject !== ProjectPermissionSub.Integrations || hasNativeIntegrations + ) .sort((a, b) => a[1].title.localeCompare(b[1].title)) .map(([subject]) => subject); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index c93cea534..f50b39a92 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -11,7 +11,11 @@ import { Button } from "@app/components/v2"; import { ProjectPermissionSub, useProject } from "@app/context"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { evaluatePermissionsAbility } from "@app/helpers/permissions"; -import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; +import { + useGetProjectRoleBySlug, + useGetWorkspaceIntegrations, + useUpdateProjectRole +} from "@app/hooks/api"; import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; @@ -105,6 +109,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { currentProject?.id ?? "", roleSlug as string ); + const { data: integrations = [] } = useGetWorkspaceIntegrations(projectId); + const hasNativeIntegrations = integrations.length > 0; const [showAccessTree, setShowAccessTree] = useState(null); @@ -198,6 +204,11 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { {!isPending && } {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]) .filter((subject) => !EXCLUDED_PERMISSION_SUBS.includes(subject)) + .filter( + (subject) => + // Hide Native Integrations policy if project has no integrations + subject !== ProjectPermissionSub.Integrations || hasNativeIntegrations + ) .map((subject) => ( { from: ROUTE_PATHS.SecretManager.IntegrationsListPage.id }); + const { data: integrations } = useGetWorkspaceIntegrations(currentProject.id); + const hasNativeIntegrations = Boolean(integrations?.length); + const updateSelectedTab = (tab: string) => { navigate({ to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path, @@ -57,7 +63,7 @@ export const IntegrationsListPage = () => {
@@ -65,15 +71,17 @@ export const IntegrationsListPage = () => { Secret Syncs - - Native Integrations - Framework Integrations Infrastructure Integrations + {hasNativeIntegrations && ( + + Native Integrations + + )} { - - - - - + {hasNativeIntegrations && ( + +
+
+ +
+

+ We're moving Native Integrations to{" "} + + Secret Syncs + + . +

+

+ If the integration you need isn't available in the Secret Syncs menu, + please get in touch with us at{" "} + + team@infisical.com + + . +

+
+
+
+ + + +
+ )}
diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx index 9332023f5..bb9d02d3d 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.utils.tsx @@ -1,10 +1,4 @@ -import crypto from "crypto"; - -import { NavigateFn } from "@tanstack/react-router"; - import { createNotification } from "@app/components/notifications"; -import { localStorageService } from "@app/helpers/localStorage"; -import { TCloudIntegration } from "@app/hooks/api/types"; export const createIntegrationMissingEnvVarsNotification = ( slug: string, @@ -27,349 +21,3 @@ export const createIntegrationMissingEnvVarsNotification = ( ), title: "Missing Environment Variables" }); - -export const redirectForProviderAuth = ( - orgId: string, - projectId: string, - navigate: NavigateFn, - integrationOption: TCloudIntegration -) => { - try { - // generate CSRF token for OAuth2 code-token exchange integrations - const state = crypto.randomBytes(16).toString("hex"); - localStorage.setItem("latestCSRFToken", state); - localStorageService.setIntegrationProjectId(projectId); - - switch (integrationOption.slug) { - case "gcp-secret-manager": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/gcp-secret-manager/authorize", - params: { - orgId, - projectId - } - }); - break; - case "azure-key-vault": { - if (!integrationOption.clientId) { - createIntegrationMissingEnvVarsNotification(integrationOption.slug); - return; - } - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-key-vault/authorize", - params: { - orgId, - projectId - }, - search: { - clientId: integrationOption.clientId, - state - } - }); - break; - } - case "azure-app-configuration": { - if (!integrationOption.clientId) { - createIntegrationMissingEnvVarsNotification(integrationOption.slug); - return; - } - const link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-app-configuration/oauth2/callback&response_mode=query&scope=https://azconfig.io/.default openid offline_access&state=${state}`; - window.location.assign(link); - break; - } - case "aws-parameter-store": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/aws-parameter-store/authorize", - params: { - orgId, - projectId - } - }); - break; - case "aws-secret-manager": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/aws-secret-manager/authorize", - params: { - orgId, - projectId - } - }); - break; - case "heroku": { - if (!integrationOption.clientId) { - createIntegrationMissingEnvVarsNotification(integrationOption.slug); - return; - } - const link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`; - window.location.assign(link); - break; - } - case "vercel": { - if (!integrationOption.clientSlug) { - createIntegrationMissingEnvVarsNotification(integrationOption.slug); - return; - } - const link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`; - window.location.assign(link); - break; - } - case "netlify": { - if (!integrationOption.clientId) { - createIntegrationMissingEnvVarsNotification(integrationOption.slug); - return; - } - const link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`; - - window.location.assign(link); - break; - } - case "github": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/github/auth-mode-selection", - params: { - orgId, - projectId - } - }); - break; - case "gitlab": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/gitlab/authorize", - params: { - orgId, - projectId - } - }); - break; - case "render": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/render/authorize", - params: { - orgId, - projectId - } - }); - break; - case "flyio": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/flyio/authorize", - params: { - orgId, - projectId - } - }); - break; - case "circleci": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/circleci/authorize", - params: { - orgId, - projectId - } - }); - break; - case "databricks": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/databricks/authorize", - params: { - orgId, - projectId - } - }); - break; - case "laravel-forge": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/laravel-forge/authorize", - params: { - orgId, - projectId - } - }); - break; - case "travisci": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/travisci/authorize", - params: { - orgId, - projectId - } - }); - break; - case "supabase": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/supabase/authorize", - params: { - orgId, - projectId - } - }); - break; - case "checkly": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/checkly/authorize", - params: { - orgId, - projectId - } - }); - break; - case "qovery": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/qovery/authorize", - params: { - orgId, - projectId - } - }); - break; - case "railway": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/railway/authorize", - params: { - orgId, - projectId - } - }); - break; - case "terraform-cloud": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/terraform-cloud/authorize", - params: { - orgId, - projectId - } - }); - break; - case "hashicorp-vault": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/hashicorp-vault/authorize", - params: { - orgId, - projectId - } - }); - break; - case "cloudflare-pages": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/cloudflare-pages/authorize", - params: { - orgId, - projectId - } - }); - break; - case "cloudflare-workers": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/cloudflare-workers/authorize", - params: { - orgId, - projectId - } - }); - break; - case "bitbucket": { - if (!integrationOption.clientId) { - createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd"); - return; - } - const link = `https://bitbucket.org/site/oauth2/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/bitbucket/oauth2/callback&state=${state}`; - window.location.assign(link); - break; - } - case "codefresh": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/codefresh/authorize", - params: { - orgId, - projectId - } - }); - break; - case "digital-ocean-app-platform": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/authorize", - params: { - orgId, - projectId - } - }); - break; - case "cloud-66": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/cloud-66/authorize", - params: { - orgId, - projectId - } - }); - break; - case "northflank": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/northflank/authorize", - params: { - orgId, - projectId - } - }); - break; - case "windmill": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/windmill/authorize", - params: { - orgId, - projectId - } - }); - break; - case "teamcity": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/teamcity/authorize", - params: { - orgId, - projectId - } - }); - break; - case "hasura-cloud": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/hasura-cloud/authorize", - params: { - orgId, - projectId - } - }); - break; - case "rundeck": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/rundeck/authorize", - params: { - orgId, - projectId - } - }); - break; - case "azure-devops": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-devops/authorize", - params: { - orgId, - projectId - } - }); - break; - case "octopus-deploy": - navigate({ - to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/octopus-deploy/authorize", - params: { - orgId, - projectId - } - }); - break; - default: - break; - } - } catch (err) { - console.error(err); - } -}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx deleted file mode 100644 index ff92f9f41..000000000 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - faCheck, - faChevronLeft, - faMagnifyingGlass, - faSearch, - faXmark -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useNavigate } from "@tanstack/react-router"; - -import { NoEnvironmentsBanner } from "@app/components/integrations/NoEnvironmentsBanner"; -import { createNotification } from "@app/components/notifications"; -import { - Button, - DeleteActionModal, - EmptyState, - Input, - Skeleton, - Tooltip -} from "@app/components/v2"; -import { ROUTE_PATHS } from "@app/const/routes"; -import { - ProjectPermissionActions, - ProjectPermissionSub, - useOrganization, - useProject, - useProjectPermission -} from "@app/context"; -import { usePopUp } from "@app/hooks"; -import { SecretSync } from "@app/hooks/api/secretSyncs"; -import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types"; -import { IntegrationsListPageTabs } from "@app/types/integrations"; - -type Props = { - isLoading?: boolean; - integrationAuths?: Record; - cloudIntegrations?: TCloudIntegration[]; - onIntegrationStart: (slug: string) => void; - // cb: handle popUpClose child->parent communication pattern - onIntegrationRevoke: (slug: string, cb: () => void) => void; - onViewActiveIntegrations?: () => void; -}; - -type TRevokeIntegrationPopUp = { provider: string }; - -const SECRET_SYNCS = Object.values(SecretSync) as string[]; -const isSecretSyncAvailable = (type: string) => SECRET_SYNCS.includes(type); - -export const CloudIntegrationSection = ({ - isLoading, - cloudIntegrations = [], - integrationAuths = {}, - onIntegrationStart, - onIntegrationRevoke, - onViewActiveIntegrations -}: Props) => { - const { t } = useTranslation(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "deleteConfirmation" - ] as const); - const { permission } = useProjectPermission(); - const { currentOrg } = useOrganization(); - const { currentProject } = useProject(); - const navigate = useNavigate(); - - const isEmpty = !isLoading && !cloudIntegrations?.length; - - const sortedCloudIntegrations = useMemo(() => { - const sortedIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name)); - - if (currentProject?.environments.length === 0) { - return sortedIntegrations.map((integration) => ({ ...integration, isAvailable: false })); - } - - return sortedIntegrations; - }, [cloudIntegrations, currentProject?.environments]); - - const [search, setSearch] = useState(""); - - const filteredIntegrations = sortedCloudIntegrations?.filter((cloudIntegration) => - cloudIntegration.name.toLowerCase().includes(search.toLowerCase().trim()) - ); - - return ( -
- {currentProject?.environments.length === 0 && ( -
- -
- )} -
- {onViewActiveIntegrations && ( - - )} -
-
-

{t("integrations.cloud-integrations")}

-

{t("integrations.click-to-start")}

-
- setSearch(e.target.value)} - leftIcon={} - placeholder="Search cloud integrations..." - containerClassName="flex-1 h-min text-base" - /> -
-
-
- {isLoading && - Array.from({ length: 12 }).map((_, index) => ( - - ))} - - {!isLoading && filteredIntegrations.length ? ( - filteredIntegrations.map((cloudIntegration) => { - const syncSlug = cloudIntegration.syncSlug ?? cloudIntegration.slug; - const isSyncAvailable = isSecretSyncAvailable(syncSlug); - - return ( -
null} - role="button" - tabIndex={0} - className={`group relative ${ - cloudIntegration.isAvailable - ? "cursor-pointer duration-200 hover:bg-mineshaft-700" - : "opacity-50" - } flex h-36 flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3`} - onClick={() => { - if (isSyncAvailable) { - navigate({ - to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path, - params: { - orgId: currentOrg.id, - projectId: currentProject.id - }, - search: { - selectedTab: IntegrationsListPageTabs.SecretSyncs, - addSync: syncSlug as SecretSync - } - }); - return; - } - if (!cloudIntegration.isAvailable) return; - if ( - permission.cannot( - ProjectPermissionActions.Create, - ProjectPermissionSub.Integrations - ) - ) { - createNotification({ - type: "error", - text: "You do not have permission to create an integration" - }); - return; - } - onIntegrationStart(cloudIntegration.slug); - }} - key={cloudIntegration.slug} - > -
- integration logo -
- {cloudIntegration.name} -
-
- {cloudIntegration.isAvailable && - Boolean(integrationAuths?.[cloudIntegration.slug]) && ( -
-
-
- - Authorized -
- -
null} - role="button" - tabIndex={0} - onClick={async (event) => { - event.stopPropagation(); - handlePopUpOpen("deleteConfirmation", { - provider: cloudIntegration.slug - }); - }} - className="absolute top-0 right-0 flex h-0 w-12 cursor-pointer items-center justify-center overflow-hidden rounded-r-md bg-red text-xs opacity-50 transition-all duration-300 group-hover:h-full hover:opacity-100" - > - -
-
-
-
- )} - {isSyncAvailable && ( -
-
-
- Secret Sync Available -
-
-
- )} -
- ); - }) - ) : ( - - )} -
- {isEmpty && ( -
- {Array.from({ length: 16 }).map((_, index) => ( -
- ))} -
- )} - handlePopUpToggle("deleteConfirmation", isOpen)} - deleteKey={(popUp?.deleteConfirmation?.data as TRevokeIntegrationPopUp)?.provider || ""} - onDeleteApproved={async () => { - onIntegrationRevoke( - (popUp.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider, - () => handlePopUpClose("deleteConfirmation") - ); - }} - /> -
- ); -}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/index.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/index.tsx deleted file mode 100644 index 62f7a006c..000000000 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/CloudIntegrationSection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CloudIntegrationSection } from "./CloudIntegrationSection"; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx index 967027857..b51c06df4 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/NativeIntegrationsTab.tsx @@ -1,11 +1,8 @@ -import { useCallback, useEffect, useState } from "react"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useNavigate } from "@tanstack/react-router"; +import { useCallback, useEffect } from "react"; import { createNotification } from "@app/components/notifications"; -import { Button, Checkbox, DeleteActionModal, Spinner } from "@app/components/v2"; -import { useOrganization, useProject } from "@app/context"; +import { Checkbox, DeleteActionModal, Spinner } from "@app/components/v2"; +import { useProject } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; import { useDeleteIntegration, @@ -17,38 +14,26 @@ import { import { IntegrationAuth } from "@app/hooks/api/integrationAuth/types"; import { TIntegration } from "@app/hooks/api/integrations/types"; -import { redirectForProviderAuth } from "../../IntegrationsListPage.utils"; -import { CloudIntegrationSection } from "../CloudIntegrationSection"; import { IntegrationsTable } from "./IntegrationsTable"; -enum IntegrationView { - List = "list", - New = "new" -} - export const NativeIntegrationsTab = () => { - const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const { environments, id: workspaceId } = currentProject; - const navigate = useNavigate(); const { data: cloudIntegrations, isPending: isCloudIntegrationsLoading } = useGetCloudIntegrations(); - const { - data: integrationAuths, - isPending: isIntegrationAuthLoading, - isFetching: isIntegrationAuthFetching - } = useGetWorkspaceAuthorizations( - workspaceId, - useCallback((data: IntegrationAuth[]) => { - const groupBy: Record = {}; - data.forEach((el) => { - groupBy[el.integration] = el; - }); - return groupBy; - }, []) - ); + const { data: integrationAuths, isFetching: isIntegrationAuthFetching } = + useGetWorkspaceAuthorizations( + workspaceId, + useCallback((data: IntegrationAuth[]) => { + const groupBy: Record = {}; + data.forEach((el) => { + groupBy[el.integration] = el; + }); + return groupBy; + }, []) + ); // mutation const { @@ -58,11 +43,8 @@ export const NativeIntegrationsTab = () => { } = useGetWorkspaceIntegrations(workspaceId); const { mutateAsync: deleteIntegration } = useDeleteIntegration(); - const { - mutateAsync: deleteIntegrationAuths, - isSuccess: isDeleteIntegrationAuthSuccess, - reset: resetDeleteIntegrationAuths - } = useDeleteIntegrationAuths(); + + const { reset: resetDeleteIntegrationAuths } = useDeleteIntegrationAuths(); const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length; const isIntegrationsEmpty = !integrations?.length; @@ -71,7 +53,6 @@ export const NativeIntegrationsTab = () => { // After the refetch is completed check if its empty. Then set bot active and reset the submit hook for isSuccess to go back to false useEffect(() => { if ( - isDeleteIntegrationAuthSuccess && !isIntegrationFetching && !isIntegrationAuthFetching && isIntegrationsAuthorizedEmpty && @@ -81,29 +62,11 @@ export const NativeIntegrationsTab = () => { } }, [ isIntegrationFetching, - isDeleteIntegrationAuthSuccess, isIntegrationAuthFetching, isIntegrationsAuthorizedEmpty, isIntegrationsEmpty ]); - const handleProviderIntegration = async (provider: string) => { - const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug); - if (!selectedCloudIntegration) return; - - try { - redirectForProviderAuth(currentOrg.id, currentProject.id, navigate, selectedCloudIntegration); - } catch (error) { - console.error(error); - } - }; - - // function to strat integration for a provider - // confirmation to user passing the bot key for provider to get secret access - const handleProviderIntegrationStart = (provider: string) => { - handleProviderIntegration(provider); - }; - const handleIntegrationDelete = async ( integrationId: string, shouldDeleteIntegrationSecrets: boolean, @@ -117,28 +80,11 @@ export const NativeIntegrationsTab = () => { }); }; - const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => { - const integrationAuthForProvider = integrationAuths?.[provider]; - if (!integrationAuthForProvider) return; - - await deleteIntegrationAuths({ - integration: provider, - workspaceId - }); - if (cb) cb(); - createNotification({ - type: "success", - text: "Revoked provider authentication" - }); - }; - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "deleteConfirmation", "deleteSecretsConfirmation" ] as const); - const [view, setView] = useState(IntegrationView.List); - const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false); if (isIntegrationLoading || isCloudIntegrationsLoading) @@ -150,18 +96,10 @@ export const NativeIntegrationsTab = () => { return ( <> - {view === IntegrationView.List ? ( + {integrations?.length && (

Native Integrations

-
{ }} />
- ) : ( - setView(IntegrationView.List)} - /> )} { } }; + const [copiedSlug, , setCopiedSlug] = useTimedReset({ + initialState: "" + }); + + const copyToClipboard = (value: string, slug: string) => { + navigator.clipboard.writeText(value); + setCopiedSlug(slug); + }; + const [filter, setFilter] = useState(DEFAULT_FILTER_STATE); const [filterHistory, setFilterHistory] = useState< Map @@ -921,12 +933,12 @@ export const OverviewPage = () => {
Inject your secrets using { , { , { , and { > {name}

- ) : ( - "" - ) +
+ {collapseEnvironments ? ( +

{name}

+ ) : ( + "" + )} +
+

{slug}

+ copyToClipboard(slug, slug)} + > + + +
+
} 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}`} >
{ - if (!isSecretValueFetched && !isDirty) { - try { - const data = await fetchSecretValue(fetchSecretValueParams); + try { + const { data } = await refetchSecretValue(); - queryClient.setQueryData(dashboardKeys.getSecretValue(fetchSecretValueParams), data); - - await window.navigator.clipboard.writeText(data.valueOverride ?? data.value); - createNotification({ type: "success", text: "Copied secret to clipboard" }); - return; - } catch (e) { - console.error(e); - createNotification({ - type: "error", - text: "Failed to fetch secret value." - }); - return; - } - } - - const { value } = getValues(); - if (value) { - try { - await window.navigator.clipboard.writeText(value); - createNotification({ type: "success", text: "Copied secret to clipboard" }); - } catch (error) { - console.log(error); - createNotification({ type: "error", text: "Failed to copy secret to clipboard" }); - } + await window.navigator.clipboard.writeText(data?.valueOverride ?? data?.value ?? ""); + createNotification({ type: "success", text: "Copied secret to clipboard" }); + } catch (e) { + console.error(e); + createNotification({ + type: "error", + text: "Failed to fetch secret value." + }); } }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 8e061291d..77ea94f99 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -121,6 +121,9 @@ const Page = () => { const tableRef = useRef(null); const [isVisible, setIsVisible] = useState(false); + const [selectedDynamicSecretId, setSelectedDynamicSecretId] = useState( + routerQueryParams.dynamicSecretId || "" + ); const { isBatchMode, pendingChanges } = useBatchMode(); const { loadPendingChanges, setExistingKeys } = useBatchModeActions(); @@ -165,6 +168,28 @@ const Page = () => { if (isVisible) setIsVisible(false); }, [environment]); + useEffect(() => { + if (routerQueryParams.dynamicSecretId !== null) { + setSelectedDynamicSecretId(routerQueryParams.dynamicSecretId); + + navigate({ + search: (prev) => ({ + ...prev, + dynamicSecretId: undefined + }) + }); + + // if any of the router query params are changed, we have to clear the selected dynamic secret id to avoid re-rendering the lease modal when it suddendly becomes available + } else { + setSelectedDynamicSecretId(null); + } + }, [ + routerQueryParams.filterBy, + routerQueryParams.search, + routerQueryParams.secretPath, + routerQueryParams.tags + ]); + const canReadSecret = hasSecretReadValueOrDescribePermission( permission, ProjectPermissionSecretActions.DescribeSecret, @@ -1039,6 +1064,7 @@ const Page = () => { )} {canReadDynamicSecret && Boolean(dynamicSecrets?.length) && ( { + if ( + selectedDynamicSecretId && + dynamicSecrets.find((secret) => secret.id === selectedDynamicSecretId) + ) { + handlePopUpOpen("dynamicSecretLeases", selectedDynamicSecretId); + } + }, [selectedDynamicSecretId]); + return ( <> {dynamicSecrets.map((secret) => { @@ -231,7 +244,12 @@ export const DynamicSecretListView = ({
+

Dynamic secret leases

+ {secret.name} +
+ } subTitle="Revoke or renew your secret leases" className="max-w-3xl" > diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx index a57995b72..95fe6edd1 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx @@ -47,13 +47,9 @@ import { faEyeSlash, faKey, faRotate, faWarning } from "@fortawesome/free-solid- import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { format } from "date-fns"; import { CreateReminderForm } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm"; -import { - dashboardKeys, - fetchSecretValue, - useGetSecretValue -} from "@app/hooks/api/dashboard/queries"; +import { useGetSecretValue } from "@app/hooks/api/dashboard/queries"; import { createNotification } from "@app/components/notifications"; -import { useQueryClient } from "@tanstack/react-query"; +import { DashboardSecretValue } from "@app/hooks/api/dashboard/types"; import { FontAwesomeSpriteName, formSchema, @@ -124,7 +120,6 @@ export const SecretItem = memo( const { removePendingChange } = useBatchModeActions(); const [isFieldFocused, setIsFieldFocused] = useToggle(); - const queryClient = useQueryClient(); const canFetchSecretValue = !originalSecret.secretValueHidden && @@ -142,7 +137,8 @@ export const SecretItem = memo( const { data: secretValueData, isPending: isPendingSecretValueData, - isError: isErrorFetchingSecretValue + isError: isErrorFetchingSecretValue, + refetch: refetchSecretValueData } = useGetSecretValue(fetchSecretValueParams, { enabled: canFetchSecretValue && (isVisible || isFieldFocused) }); @@ -210,7 +206,6 @@ export const SecretItem = memo( watch, setValue, reset, - getValues, trigger, formState: { isDirty, isSubmitting, errors }, getFieldState @@ -386,37 +381,25 @@ export const SecretItem = memo( } }; - const fetchValue = async () => { - if (secretValueData) return secretValueData; - - try { - const data = await fetchSecretValue(fetchSecretValueParams); - - queryClient.setQueryData(dashboardKeys.getSecretValue(fetchSecretValueParams), data); - - return data; - } catch (e) { - console.error(e); + const fetchValue = async (): Promise => { + const { data, isRefetchError } = await refetchSecretValueData(); + if (isRefetchError) { createNotification({ type: "error", text: "Failed to fetch secret value" }); - throw e; } + if (!data) return undefined; + + return data; }; const copyTokenToClipboard = async () => { - if (hasFetchedSecretValue) { - const [overrideValue, value] = getValues(["value", "valueOverride"]); - if (isOverridden) { - navigator.clipboard.writeText(value as string); - } else { - navigator.clipboard.writeText(overrideValue as string); - } - } else { - const data = await fetchValue(); - navigator.clipboard.writeText((data.valueOverride ?? data.value) as string); - } + const data = await fetchValue(); + if (!data) return; + + navigator.clipboard.writeText(data.valueOverride ?? data.value); + setIsSecValueCopied.on(); }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx index f5796fd69..df054b6e2 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx @@ -12,6 +12,7 @@ const SecretDashboardPageQueryParamsSchema = z.object({ search: z.string().catch(""), tags: z.string().catch(""), filterBy: z.string().catch(""), + dynamicSecretId: z.string().catch(""), connectionId: z.string().optional(), connectionName: z.string().optional() }); @@ -26,7 +27,8 @@ export const Route = createFileRoute( secretPath: "/", search: "", tags: "", - filterBy: "" + filterBy: "", + dynamicSecretId: "" }) ] }, diff --git a/frontend/src/pages/secret-manager/SettingsPage/SettingsPage.tsx b/frontend/src/pages/secret-manager/SettingsPage/SettingsPage.tsx index b2d984d75..6f437a738 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/SettingsPage.tsx @@ -1,5 +1,7 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; +import { Link } from "@tanstack/react-router"; +import { InfoIcon } from "lucide-react"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useProject } from "@app/context"; @@ -43,9 +45,19 @@ export const SettingsPage = () => {
+ > + + Looking for organization settings? + + {tabs diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx index 70c486616..5ff1a1535 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/EnvironmentSection/AddEnvironmentModal.tsx @@ -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({ + const { + control, + handleSubmit, + setValue, + getValues, + formState: { dirtyFields } + } = useForm({ 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 ( ( + render={({ field: { onChange, ...field }, fieldState: { error } }) => ( - + { + onChange(e); + handleEnvironmentNameChange(); + }} + /> )} /> diff --git a/frontend/src/pages/secret-scanning/SettingsPage/SettingsPage.tsx b/frontend/src/pages/secret-scanning/SettingsPage/SettingsPage.tsx index 183c6d285..c4b03d03b 100644 --- a/frontend/src/pages/secret-scanning/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/secret-scanning/SettingsPage/SettingsPage.tsx @@ -1,9 +1,11 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; +import { Link } from "@tanstack/react-router"; +import { InfoIcon } from "lucide-react"; import { ProjectPermissionCan } from "@app/components/permissions"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; -import { ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionSub, useOrganization } from "@app/context"; import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab"; @@ -12,6 +14,7 @@ import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab" export const SettingsPage = () => { const { t } = useTranslation(); + const { currentOrg } = useOrganization(); return (
@@ -21,9 +24,19 @@ export const SettingsPage = () => {
+ > + + Looking for organization settings? + + diff --git a/frontend/src/pages/ssh/SettingsPage/SettingsPage.tsx b/frontend/src/pages/ssh/SettingsPage/SettingsPage.tsx index a154a8391..b01dfdcf4 100644 --- a/frontend/src/pages/ssh/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/ssh/SettingsPage/SettingsPage.tsx @@ -1,9 +1,11 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; +import { Link } from "@tanstack/react-router"; +import { InfoIcon } from "lucide-react"; import { ProjectPermissionCan } from "@app/components/permissions"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useOrganization } from "@app/context"; import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab"; @@ -12,6 +14,8 @@ import { ProjectSshTab } from "./components/ProjectSshTab"; export const SettingsPage = () => { const { t } = useTranslation(); + const { currentOrg } = useOrganization(); + return (
@@ -20,9 +24,19 @@ export const SettingsPage = () => {
+ > + + Looking for organization settings? + + diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index f2954f03c..ca413ef21 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -1515,8 +1515,8 @@ const certManagerPkiSyncDetailsByIDPageRouteRoute = const certManagerCertAuthDetailsByIDPageRouteRoute = certManagerCertAuthDetailsByIDPageRouteImport.update({ - id: '/ca/$caName', - path: '/ca/$caName', + id: '/ca/$caId', + path: '/ca/$caId', getParentRoute: () => certManagerLayoutRoute, } as any) @@ -3182,10 +3182,10 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof secretScanningSecretScanningDataSourcesPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationsOrgIdProjectsSecretScanningProjectIdSecretScanningLayoutDataSourcesImport } - '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName': { - id: '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName' - path: '/ca/$caName' - fullPath: '/organizations/$orgId/projects/cert-management/$projectId/ca/$caName' + '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId': { + id: '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId' + path: '/ca/$caId' + fullPath: '/organizations/$orgId/projects/cert-management/$projectId/ca/$caId' preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport parentRoute: typeof certManagerLayoutImport } @@ -5199,7 +5199,7 @@ export interface FileRoutesByFullPath { '/organizations/$orgId/projects/pam/$projectId/sessions/': typeof pamPamSessionsPageRouteRoute '/organizations/$orgId/projects/secret-management/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/organizations/$orgId/projects/secret-scanning/$projectId/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute - '/organizations/$orgId/projects/cert-management/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/organizations/$orgId/projects/cert-management/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute '/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute '/organizations/$orgId/projects/cert-management/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId': typeof pamPamSessionsByIDPageRouteRoute @@ -5429,7 +5429,7 @@ export interface FileRoutesByTo { '/organizations/$orgId/projects/pam/$projectId/sessions': typeof pamPamSessionsPageRouteRoute '/organizations/$orgId/projects/secret-management/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute '/organizations/$orgId/projects/secret-scanning/$projectId/data-sources': typeof secretScanningSecretScanningDataSourcesPageRouteRoute - '/organizations/$orgId/projects/cert-management/$projectId/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/organizations/$orgId/projects/cert-management/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute '/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute '/organizations/$orgId/projects/cert-management/$projectId/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId': typeof pamPamSessionsByIDPageRouteRoute @@ -5682,7 +5682,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/': typeof pamPamSessionsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/': typeof secretScanningSecretScanningDataSourcesPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId': typeof certManagerPkiSyncDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId': typeof pamPamSessionsByIDPageRouteRoute @@ -5927,7 +5927,7 @@ export interface FileRouteTypes { | '/organizations/$orgId/projects/pam/$projectId/sessions/' | '/organizations/$orgId/projects/secret-management/$projectId/integrations/' | '/organizations/$orgId/projects/secret-scanning/$projectId/data-sources/' - | '/organizations/$orgId/projects/cert-management/$projectId/ca/$caName' + | '/organizations/$orgId/projects/cert-management/$projectId/ca/$caId' | '/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId' | '/organizations/$orgId/projects/cert-management/$projectId/subscribers/$subscriberName' | '/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId' @@ -6156,7 +6156,7 @@ export interface FileRouteTypes { | '/organizations/$orgId/projects/pam/$projectId/sessions' | '/organizations/$orgId/projects/secret-management/$projectId/integrations' | '/organizations/$orgId/projects/secret-scanning/$projectId/data-sources' - | '/organizations/$orgId/projects/cert-management/$projectId/ca/$caName' + | '/organizations/$orgId/projects/cert-management/$projectId/ca/$caId' | '/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId' | '/organizations/$orgId/projects/cert-management/$projectId/subscribers/$subscriberName' | '/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId' @@ -6407,7 +6407,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/' | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/' | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/' - | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName' + | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId' | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId' | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName' | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId' @@ -7030,7 +7030,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates", "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations", "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers", - "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId", "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/members/$membershipId", @@ -7415,7 +7415,7 @@ export const routeTree = rootRoute "filePath": "secret-scanning/SecretScanningDataSourcesPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources" }, - "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName": { + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId": { "filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout" }, diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 265c64649..b0b52c4d3 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -282,7 +282,7 @@ const certManagerRoutes = route("/organizations/$orgId/projects/cert-management/ route("/certificate-templates", [index("cert-manager/PkiTemplateListPage/route.tsx")]), route("/certificate-authorities", "cert-manager/CertificateAuthoritiesPage/route.tsx"), route("/alerting", "cert-manager/AlertingPage/route.tsx"), - route("/ca/$caName", "cert-manager/CertAuthDetailsByIDPage/route.tsx"), + route("/ca/$caId", "cert-manager/CertAuthDetailsByIDPage/route.tsx"), route("/pki-collections/$collectionId", "cert-manager/PkiCollectionDetailsByIDPage/routes.tsx"), route("/integrations", [ index("cert-manager/IntegrationsListPage/route.tsx"), diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 618dcb618..ace4852ac 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -23,6 +23,11 @@ const virtualRouteFileChangeReloadPlugin: PluginOption = { export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd()); const allowedHosts = env.VITE_ALLOWED_HOSTS?.split(",") ?? []; + const version = ( + env.INFISICAL_PLATFORM_VERSION || + env.VITE_INFISICAL_PLATFORM_VERSION || + "0.0.1" + ).replaceAll(".", "-"); return { server: { @@ -38,6 +43,15 @@ export default defineConfig(({ mode }) => { // } // } }, + build: { + rollupOptions: { + output: { + entryFileNames: `assets/[name]-${version}-[hash].js`, + chunkFileNames: `assets/[name]-${version}-[hash].js`, + assetFileNames: `assets/[name]-${version}-[hash].[ext]` + } + } + }, plugins: [ tsconfigPaths(), nodePolyfills({