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/.infisicalignore b/.infisicalignore index 7a07a9504..d0f302ce0 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -57,3 +57,4 @@ docs/documentation/platform/pki/enrollment-methods/api.mdx:generic-api-key:93 docs/documentation/platform/pki/enrollment-methods/api.mdx:private-key:139 docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx:private-key:62 docs/documentation/platform/pki/certificate-syncs/chef.mdx:private-key:61 +backend/src/services/certificate-request/certificate-request-service.test.ts:private-key:246 \ No newline at end of file diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index ab1d6fbb7..9302578fe 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -185,6 +185,9 @@ COPY --from=backend-runner /app /backend COPY --from=frontend-runner /app ./backend/frontend-build +# Make export-assets script executable for CDN asset extraction +RUN chmod +x /backend/scripts/export-assets.sh + ARG INFISICAL_PLATFORM_VERSION ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 01c9a737b..faf489b2d 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 @@ -173,6 +174,9 @@ ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY COPY --from=backend-runner /app /backend COPY --from=frontend-runner /app ./backend/frontend-build +# Make export-assets script executable for CDN asset extraction +RUN chmod +x /backend/scripts/export-assets.sh + ARG INFISICAL_PLATFORM_VERSION ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION 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..80f6fed6c 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 @@ -22,9 +22,31 @@ Feature: Challenge And I parse the full-chain certificate from order finalized_order as cert And the value cert with jq ".subject.common_name" should be equal to "localhost" + Scenario: Validate challenge with retry + Given I have an ACME cert profile 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 + """ + { + "COMMON_NAME": "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 wait 45 seconds and serve challenge response for challenge at localhost + And I tell ACME server that challenge is ready to be verified + And I poll and finalize the ACME order order as finalized_order + And the value finalized_order.body with jq ".status" should be equal to "valid" + And I parse the full-chain certificate from order finalized_order as cert + And the value cert with jq ".subject.common_name" should be equal to "localhost" + 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,18 +80,17 @@ 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 """ - { - "COMMON_NAME": "localhost" - } + {} """ And I add subject alternative name to certificate signing request csr """ [ + "localhost", "infisical.com" ] """ @@ -82,56 +103,19 @@ Feature: Challenge # the localhost auth should be valid And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "localhost")) | first | .uri" as localhost_auth - And I peak and memorize the next nonce as nonce - When I send a raw ACME request to "{localhost_auth}" - """ - { - "protected": { - "alg": "RS256", - "nonce": "{nonce}", - "url": "{localhost_auth}", - "kid": "{acme_account.uri}" - } - } - """ - Then the value response.status_code should be equal to 200 - And the value response with jq ".status" should be equal to "valid" + And I wait until the status of authorization localhost_auth becomes valid # the infisical.com auth should still be pending And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "infisical.com")) | first | .uri" as infisical_auth - And I memorize response.headers with jq ".["replay-nonce"]" as nonce - When I send a raw ACME request to "{infisical_auth}" - """ - { - "protected": { - "alg": "RS256", - "nonce": "{nonce}", - "url": "{infisical_auth}", - "kid": "{acme_account.uri}" - } - } - """ - Then the value response.status_code should be equal to 200 - And the value response with jq ".status" should be equal to "pending" + And I post-as-get {infisical_auth} as infisical_auth_resp + And the value infisical_auth_resp with jq ".status" should be equal to "pending" # the order should be pending as well - And I memorize response.headers with jq ".["replay-nonce"]" as nonce - When I send a raw ACME request to "{order.uri}" - """ - { - "protected": { - "alg": "RS256", - "nonce": "{nonce}", - "url": "{order.uri}", - "kid": "{acme_account.uri}" - } - } - """ - Then the value response.status_code should be equal to 200 - And the value response with jq ".status" should be equal to "pending" + And I post-as-get {order.uri} as order_resp + And the value order_resp with jq ".status" should be equal to "pending" # finalize should not be allowed when all auths are not valid yet - And I memorize response.headers with jq ".["replay-nonce"]" as nonce + And I get a new-nonce as nonce When I send a raw ACME request to "{order.body.finalize}" """ { @@ -153,7 +137,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 +149,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": { @@ -185,8 +169,10 @@ Feature: Challenge Then the value response.status_code should be equal to 201 And I memorize response with jq ".finalize" as finalize_url And I memorize response.headers with jq ".["replay-nonce"]" as nonce + And I memorize response.headers with jq ".["location"]" as order_uri And I memorize response as order And I pass all challenges with type http-01 for order in order + And I wait until the status of order order_uri becomes ready And I encode CSR csr_pem as JOSE Base-64 DER as base64_csr_der When I send a raw ACME request to "{finalize_url}" """ 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..5a2cef0cc 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,542 @@ 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"} | + | {} | + + Scenario Outline: Issue a certificate with bad CSR names disallowed by the template + 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 + """ + { + "dnsProviderConfig": { + "provider": "cloudflare", + "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": [ + "example.com" + ] + } + ], + "sans": [ + { + "type": "dns_name", + "allowed": [ + "infisical.com" + ] + } + ], + "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 + """ + + """ + Then I add subject alternative name to certificate signing request csr + """ + + """ + 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 pass all challenges with type http-01 for order in order + Given I intercept outgoing requests + """ + [ + { + "scope": "https://api.cloudflare.com:443", + "method": "POST", + "path": "/client/v4/zones/MOCK_ZONE_ID/dns_records", + "status": 200, + "response": { + "result": { + "id": "A2A6347F-88B5-442D-9798-95E408BC7701", + "name": "Mock Account", + "type": "standard", + "settings": { + "enforce_twofactor": false, + "api_access_enabled": null, + "access_approval_expiry": null, + "abuse_contact_email": null, + "user_groups_ui_beta": false + }, + "legacy_flags": { + "enterprise_zone_quota": { + "maximum": 0, + "current": 0, + "available": 0 + } + }, + "created_on": "2013-04-18T00:41:02.215243Z" + }, + "success": true, + "errors": [], + "messages": [] + }, + "responseIsBinary": false + }, + { + "scope": "https://api.cloudflare.com:443", + "method": "GET", + "path": { + "regex": "/client/v4/zones/[^/]+/dns_records\\?" + }, + "status": 200, + "response": { + "result": [], + "success": true, + "errors": [], + "messages": [], + "result_info": { + "page": 1, + "per_page": 100, + "count": 0, + "total_count": 0, + "total_pages": 1 + } + }, + "responseIsBinary": false + } + ] + """ + Then I poll and finalize the ACME order order as finalized_order + And the value error.typ should be equal to "urn:ietf:params:acme:error:badCSR" + And the value error.detail should be equal to "" + + Examples: + | subject | san | err_detail | + | {"COMMON_NAME": "localhost"} | [] | Invalid CSR: common_name value 'localhost' is not in allowed values list | + | {"COMMON_NAME": "localhost"} | ["infisical.com"] | Invalid CSR: common_name value 'localhost' is not in allowed values list | + | {} | ["localhost"] | Invalid CSR: dns_name SAN value 'localhost' is not in allowed values list | + | {} | ["infisical.com", "localhost"] | Invalid CSR: dns_name SAN value 'localhost' is not in allowed values list | + | {"COMMON_NAME": "example.com"} | ["infisical.com", "localhost"] | Invalid CSR: dns_name SAN value 'localhost' is not in allowed values list | + + + Scenario Outline: Issue a certificate with algorithms disallowed by the template + 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 + """ + { + "dnsProviderConfig": { + "provider": "cloudflare", + "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": [ + "" + ], + "keyAlgorithm": [ + "" + ] + }, + "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 + """ + {} + """ + Then I add subject alternative name to certificate signing request csr + """ + [ + "localhost" + ] + """ + And I create a private key pair as cert_key + And I sign the certificate signing request csr with "" hash and 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 pass all challenges with type http-01 for order in order + Given I intercept outgoing requests + """ + [ + { + "scope": "https://api.cloudflare.com:443", + "method": "POST", + "path": "/client/v4/zones/MOCK_ZONE_ID/dns_records", + "status": 200, + "response": { + "result": { + "id": "A2A6347F-88B5-442D-9798-95E408BC7701", + "name": "Mock Account", + "type": "standard", + "settings": { + "enforce_twofactor": false, + "api_access_enabled": null, + "access_approval_expiry": null, + "abuse_contact_email": null, + "user_groups_ui_beta": false + }, + "legacy_flags": { + "enterprise_zone_quota": { + "maximum": 0, + "current": 0, + "available": 0 + } + }, + "created_on": "2013-04-18T00:41:02.215243Z" + }, + "success": true, + "errors": [], + "messages": [] + }, + "responseIsBinary": false + }, + { + "scope": "https://api.cloudflare.com:443", + "method": "GET", + "path": { + "regex": "/client/v4/zones/[^/]+/dns_records\\?" + }, + "status": 200, + "response": { + "result": [], + "success": true, + "errors": [], + "messages": [], + "result_info": { + "page": 1, + "per_page": 100, + "count": 0, + "total_count": 0, + "total_pages": 1 + } + }, + "responseIsBinary": false + } + ] + """ + Then I poll and finalize the ACME order order as finalized_order + And the value error.typ should be equal to "urn:ietf:params:acme:error:badCSR" + And the value error.detail should be equal to "" + + Examples: + | allowed_alg | allowed_signature | key_type | hash_type | err_detail | + | RSA-4096 | SHA512-RSA | RSA-2048 | SHA512 | Invalid CSR: Key algorithm 'RSA_2048' is not allowed by template policy | + | RSA-4096 | SHA512-RSA | RSA-3072 | SHA512 | Invalid CSR: Key algorithm 'RSA_3072' is not allowed by template policy | + | RSA-4096 | ECDSA-SHA512 | ECDSA-P256 | SHA512 | Invalid CSR: Key algorithm 'EC_prime256v1' is not allowed by template policy | + | RSA-4096 | ECDSA-SHA512 | ECDSA-P384 | SHA512 | Invalid CSR: Key algorithm 'EC_secp384r1' is not allowed by template policy | + | RSA-4096 | ECDSA-SHA512 | ECDSA-P521 | SHA512 | Invalid CSR: Key algorithm 'EC_secp521r1' is not allowed by template policy | + | RSA-2048 | SHA512-RSA | RSA-2048 | SHA384 | Invalid CSR: Signature algorithm 'RSA-SHA384' is not allowed by template policy | + | RSA-2048 | SHA512-RSA | RSA-2048 | SHA256 | Invalid CSR: Signature algorithm 'RSA-SHA256' is not allowed by template policy | + | ECDSA-P256 | SHA512-RSA | ECDSA-P256 | SHA256 | Invalid CSR: Signature algorithm 'ECDSA-SHA256' is not allowed by template policy | + | ECDSA-P384 | SHA512-RSA | ECDSA-P384 | SHA256 | Invalid CSR: Signature algorithm 'ECDSA-SHA256' is not allowed by template policy | + | ECDSA-P521 | SHA512-RSA | ECDSA-P521 | SHA256 | Invalid CSR: Signature algorithm 'ECDSA-SHA256' is not allowed by template policy | + | RSA-2048 | SHA512-RSA | RSA-2048 | SHA256 | Invalid CSR: Signature algorithm 'RSA-SHA256' is not allowed by template policy | + | RSA-2048 | SHA512-RSA | RSA-4096 | SHA256 | Invalid CSR: Signature algorithm 'RSA-SHA256' is not allowed by template policy, Key algorithm 'RSA_4096' is not allowed by template policy | diff --git a/backend/bdd/features/pki/acme/internal-ca.feature b/backend/bdd/features/pki/acme/internal-ca.feature new file mode 100644 index 000000000..934b7bef3 --- /dev/null +++ b/backend/bdd/features/pki/acme/internal-ca.feature @@ -0,0 +1,33 @@ +Feature: Internal CA + + Scenario: CSR with SANs only + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to "{BASE_URL}/api/v1/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 + """ + {} + """ + And I add subject alternative name to certificate signing request csr + """ + [ + "localhost" + ] + """ + And I create a RSA private key pair as cert_key + And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + And I select challenge with type http-01 for domain localhost from order in order as challenge + And I serve challenge response for challenge at localhost + And I tell ACME server that challenge is ready to be verified + And I poll and finalize the ACME order order as finalized_order + And the value finalized_order.body with jq ".status" should be equal to "valid" + And I parse the full-chain certificate from order finalized_order as cert + And the value cert with jq ".subject.common_name" should be equal to null + And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json + """ + [ + "localhost" + ] + """ \ No newline at end of file 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..c0b2fee8f 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -2,6 +2,8 @@ import json import logging import re import urllib.parse +import time +import threading import acme.client import jq @@ -18,6 +20,10 @@ from josepy.jwk import JWKRSA from josepy import json_util from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.types import ( + CertificateIssuerPrivateKeyTypes, +) from cryptography import x509 from cryptography.x509.oid import NameOID from cryptography.hazmat.primitives import hashes @@ -56,7 +62,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 +80,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 +153,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 +214,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 +234,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 +252,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 +276,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 +294,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() @@ -561,12 +601,57 @@ def step_impl(context: Context, csr_var: str): ) -@then("I create a RSA private key pair as {rsa_key_var}") -def step_impl(context: Context, rsa_key_var: str): - context.vars[rsa_key_var] = rsa.generate_private_key( - # TODO: make them configurable if we need to - public_exponent=65537, - key_size=2048, +def gen_private_key(key_type: str): + if key_type == "RSA-2048" or key_type == "RSA": + return rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + elif key_type == "RSA-3072": + return rsa.generate_private_key( + public_exponent=65537, + key_size=3072, + ) + elif key_type == "RSA-4096": + return rsa.generate_private_key( + public_exponent=65537, + key_size=4096, + ) + elif key_type == "ECDSA-P256": + return ec.generate_private_key(curve=ec.SECP256R1()) + elif key_type == "ECDSA-P384": + return ec.generate_private_key(curve=ec.SECP384R1()) + elif key_type == "ECDSA-P521": + return ec.generate_private_key(curve=ec.SECP521R1()) + else: + raise Exception(f"Unknown key type {key_type}") + + +@then("I create a {key_type} private key pair as {rsa_key_var}") +def step_impl(context: Context, key_type: str, rsa_key_var: str): + context.vars[rsa_key_var] = gen_private_key(key_type) + + +def sign_csr( + pem: x509.CertificateSigningRequestBuilder, + pk: CertificateIssuerPrivateKeyTypes, + hash_type: str = "SHA256", +): + return pem.sign(pk, getattr(hashes, hash_type)()).public_bytes( + serialization.Encoding.PEM + ) + + +@then( + 'I sign the certificate signing request {csr_var} with "{hash_type}" hash and private key {pk_var} and output it as {pem_var} in PEM format' +) +def step_impl( + context: Context, csr_var: str, hash_type: str, pk_var: str, pem_var: str +): + context.vars[pem_var] = sign_csr( + pem=context.vars[csr_var], + pk=context.vars[pk_var], + hash_type=hash_type, ) @@ -574,10 +659,9 @@ def step_impl(context: Context, rsa_key_var: str): "I sign the certificate signing request {csr_var} with private key {pk_var} and output it as {pem_var} in PEM format" ) def step_impl(context: Context, csr_var: str, pk_var: str, pem_var: str): - context.vars[pem_var] = ( - context.vars[csr_var] - .sign(context.vars[pk_var], hashes.SHA256()) - .public_bytes(serialization.Encoding.PEM) + context.vars[pem_var] = sign_csr( + pem=context.vars[csr_var], + pk=context.vars[pk_var], ) @@ -690,6 +774,15 @@ def step_impl(context: Context, var_path: str, jq_query, var_name: str): context.vars[var_name] = value +@then("I get a new-nonce as {var_name}") +def step_impl(context: Context, var_name: str): + acme_client = context.acme_client + nonce = acme_client.net._get_nonce( + url=None, new_nonce_url=acme_client.directory.newNonce + ) + context.vars[var_name] = json_util.encode_b64jose(nonce) + + @then("I peak and memorize the next nonce as {var_name}") def step_impl(context: Context, var_name: str): acme_client = context.acme_client @@ -763,22 +856,39 @@ def select_challenge( return challenges[0] -def serve_challenge( +def serve_challenges( context: Context, - challenge: messages.ChallengeBody, + challenges: list[messages.ChallengeBody], + wait_time: int | None = None, ): if hasattr(context, "web_server"): context.web_server.shutdown_and_server_close() - response, validation = challenge.response_and_validation( - context.acme_client.net.key - ) - resource = standalone.HTTP01RequestHandler.HTTP01Resource( - chall=challenge.chall, response=response, validation=validation - ) + resources = set() + for challenge in challenges: + response, validation = challenge.response_and_validation( + context.acme_client.net.key + ) + resources.add( + standalone.HTTP01RequestHandler.HTTP01Resource( + chall=challenge.chall, response=response, validation=validation + ) + ) # TODO: make port configurable - servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), {resource}) - servers.serve_forever() + servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), resources) + if wait_time is None: + servers.serve_forever() + else: + + def wait_and_start(): + logger.info("Waiting %s seconds before we start serving.", wait_time) + time.sleep(wait_time) + logger.info("Start server now") + servers.serve_forever() + + thread = threading.Thread(target=wait_and_start) + thread.daemon = True + thread.start() context.web_server = servers @@ -831,6 +941,7 @@ def step_impl( f"Expected OrderResource but got {type(order)!r} at {order_var_path!r}" ) + challenges = {} for domain in order.body.identifiers: logger.info( "Selecting challenge for domain %s with type %s ...", @@ -855,18 +966,28 @@ def step_impl( domain.value, challenge_type, ) - serve_challenge(context=context, challenge=challenge) + challenges[domain] = challenge + serve_challenges(context=context, challenges=list(challenges.values())) + for domain, challenge in challenges.items(): logger.info( "Notifying challenge for domain %s with type %s ...", domain, challenge_type ) notify_challenge_ready(context=context, challenge=challenge) +@then( + "I wait {wait_time} seconds and serve challenge response for {var_path} at {hostname}" +) +def step_impl(context: Context, wait_time: str, var_path: str, hostname: str): + challenge = eval_var(context, var_path, as_json=False) + serve_challenges(context=context, challenges=[challenge], wait_time=int(wait_time)) + + @then("I serve challenge response for {var_path} at {hostname}") def step_impl(context: Context, var_path: str, hostname: str): challenge = eval_var(context, var_path, as_json=False) - serve_challenge(context=context, challenge=challenge) + serve_challenges(context=context, challenges=[challenge]) @then("I tell ACME server that {var_path} is ready to be verified") @@ -875,12 +996,57 @@ def step_impl(context: Context, var_path: str): notify_challenge_ready(context=context, challenge=challenge) +@then("I wait until the status of order {order_var} becomes {status}") +def step_impl(context: Context, order_var: str, status: str): + acme_client = context.acme_client + attempt_count = 6 + while attempt_count: + order = eval_var(context, order_var, as_json=False) + response = acme_client._post_as_get( + order.uri if isinstance(order, messages.OrderResource) else order + ) + order = messages.Order.from_json(response.json()) + if order.status.name == status: + return + attempt_count -= 1 + time.sleep(10) + raise TimeoutError(f"The status of order doesn't become {status} before timeout") + + +@then("I wait until the status of authorization {auth_var} becomes {status}") +def step_impl(context: Context, auth_var: str, status: str): + acme_client = context.acme_client + attempt_count = 6 + while attempt_count: + auth = eval_var(context, auth_var, as_json=False) + response = acme_client._post_as_get( + auth.uri if isinstance(auth, messages.Authorization) else auth + ) + auth = messages.Authorization.from_json(response.json()) + if auth.status.name == status: + return + attempt_count -= 1 + time.sleep(10) + raise TimeoutError(f"The status of auth doesn't become {status} before timeout") + + +@then("I post-as-get {uri} as {resp_var}") +def step_impl(context: Context, uri: str, resp_var: str): + acme_client = context.acme_client + response = acme_client._post_as_get(replace_vars(uri, vars=context.vars)) + context.vars[resp_var] = response.json() + + @then("I poll and finalize the ACME order {var_path} as {finalized_var}") def step_impl(context: Context, var_path: str, finalized_var: str): order = eval_var(context, var_path, as_json=False) acme_client = context.acme_client - finalized_order = acme_client.poll_and_finalize(order) - context.vars[finalized_var] = finalized_order + try: + finalized_order = acme_client.poll_and_finalize(order) + context.vars[finalized_var] = finalized_order + except Exception as exp: + logger.error(f"Failed to finalize order: {exp}", exc_info=True) + context.vars["error"] = exp @then("I parse the full-chain certificate from order {order_var_path} as {cert_var}") diff --git a/backend/package.json b/backend/package.json index 0e17bb2b7..4f9cfdc97 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,6 +25,7 @@ "outputPath": "binary" }, "scripts": { + "assets:export": "./scripts/export-assets.sh", "binary:build": "npm run binary:clean && npm run build:frontend && npm run build && npm run binary:babel-frontend && npm run binary:babel-backend && npm run binary:rename-imports", "binary:package": "pkg --no-bytecode --public-packages \"*\" --public --target host .", "binary:babel-backend": " babel ./dist -d ./dist", diff --git a/backend/scripts/export-assets.sh b/backend/scripts/export-assets.sh new file mode 100644 index 000000000..149700579 --- /dev/null +++ b/backend/scripts/export-assets.sh @@ -0,0 +1,75 @@ +#!/bin/sh +# Export frontend static assets for CDN deployment +# Usage: +# npm run assets:export - Output tar to stdout (pipe to file or aws s3) +# npm run assets:export /path - Extract assets to specified directory +# npm run assets:export -- --help - Show usage + +set -e + +ASSETS_PATH="/backend/frontend-build/assets" + +show_help() { + cat << 'EOF' +Export frontend static assets for CDN deployment. + +USAGE: + docker run --rm infisical/infisical npm run --silent assets:export [-- OPTIONS] [PATH] + +OPTIONS: + --help, -h Show this help message + +ARGUMENTS: + PATH Directory to export assets to. If not provided, outputs + a tar archive to stdout. + +NOTE: + Use --silent flag to suppress npm output when piping to stdout. + +EXAMPLES: + # Export as tar to local file + docker run --rm infisical/infisical npm run --silent assets:export > assets.tar + + # Extract to local directory + docker run --rm -v $(pwd)/cdn-assets:/output infisical/infisical npm run --silent assets:export /output + +EOF + exit 0 +} + +# Check for help flag +case "${1:-}" in + --help|-h) + show_help + ;; +esac + +# Verify assets exist +if [ ! -d "$ASSETS_PATH" ]; then + echo "Error: Assets directory not found at $ASSETS_PATH" >&2 + echo "Make sure the frontend is built and included in the image." >&2 + exit 1 +fi + +ASSET_COUNT=$(find "$ASSETS_PATH" -type f | wc -l | tr -d ' ') + +if [ $# -eq 0 ]; then + # No path provided - output tar to stdout + echo "Exporting $ASSET_COUNT assets as tar archive to stdout..." >&2 + tar -cf - -C "$(dirname "$ASSETS_PATH")" "$(basename "$ASSETS_PATH")" +else + # Path provided - extract to directory + OUTPUT_PATH="$1" + + if [ ! -d "$OUTPUT_PATH" ]; then + echo "Creating output directory: $OUTPUT_PATH" >&2 + mkdir -p "$OUTPUT_PATH" + fi + + echo "Exporting $ASSET_COUNT assets to $OUTPUT_PATH..." >&2 + cp -r "$ASSETS_PATH"/* "$OUTPUT_PATH/" + + echo "✅ Assets exported successfully!" >&2 + echo " Path: $OUTPUT_PATH" >&2 + echo " Files: $ASSET_COUNT assets" >&2 +fi diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 6ef775f90..02394de4d 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -65,6 +65,7 @@ import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-a import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TCertificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service"; import { TCertificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service"; +import { TCertificateRequestServiceFactory } from "@app/services/certificate-request/certificate-request-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; @@ -288,6 +289,7 @@ declare module "fastify" { auditLogStream: TAuditLogStreamServiceFactory; certificate: TCertificateServiceFactory; certificateV3: TCertificateV3ServiceFactory; + certificateRequest: TCertificateRequestServiceFactory; certificateTemplate: TCertificateTemplateServiceFactory; certificateTemplateV2: TCertificateTemplateV2ServiceFactory; certificateProfile: TCertificateProfileServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 603df5f6c..4bdd3849d 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -573,6 +573,11 @@ import { TWorkflowIntegrationsInsert, TWorkflowIntegrationsUpdate } from "@app/db/schemas"; +import { + TCertificateRequests, + TCertificateRequestsInsert, + TCertificateRequestsUpdate +} from "@app/db/schemas/certificate-requests"; import { TAccessApprovalPoliciesEnvironments, TAccessApprovalPoliciesEnvironmentsInsert, @@ -714,6 +719,11 @@ declare module "knex/types/tables" { TExternalCertificateAuthoritiesUpdate >; [TableName.Certificate]: KnexOriginal.CompositeTableType; + [TableName.CertificateRequests]: KnexOriginal.CompositeTableType< + TCertificateRequests, + TCertificateRequestsInsert, + TCertificateRequestsUpdate + >; [TableName.CertificateTemplate]: KnexOriginal.CompositeTableType< TCertificateTemplates, TCertificateTemplatesInsert, diff --git a/backend/src/db/migrations/20251121124532_add-issuer-type-to-certificate-profiles.ts b/backend/src/db/migrations/20251121124532_add-issuer-type-to-certificate-profiles.ts new file mode 100644 index 000000000..61dcdb12e --- /dev/null +++ b/backend/src/db/migrations/20251121124532_add-issuer-type-to-certificate-profiles.ts @@ -0,0 +1,27 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasIssuerTypeColumn = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "issuerType"); + + if (!hasIssuerTypeColumn) { + await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { + t.string("issuerType").notNullable().defaultTo("ca"); + }); + } + + await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { + t.uuid("caId").nullable().alter(); + }); +} + +export async function down(knex: Knex): Promise { + const hasIssuerTypeColumn = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "issuerType"); + + if (hasIssuerTypeColumn) { + await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { + t.dropColumn("issuerType"); + }); + } +} 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/migrations/20251127120000_add-certificate-requests.ts b/backend/src/db/migrations/20251127120000_add-certificate-requests.ts new file mode 100644 index 000000000..32944c37d --- /dev/null +++ b/backend/src/db/migrations/20251127120000_add-certificate-requests.ts @@ -0,0 +1,47 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.CertificateRequests))) { + await knex.schema.createTable(TableName.CertificateRequests, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.string("status").notNullable(); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("profileId").nullable(); + t.foreign("profileId").references("id").inTable(TableName.PkiCertificateProfile).onDelete("SET NULL"); + t.uuid("caId").nullable(); + t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("SET NULL"); + t.uuid("certificateId").nullable(); + t.foreign("certificateId").references("id").inTable(TableName.Certificate).onDelete("SET NULL"); + t.text("csr").nullable(); + t.string("commonName").nullable(); + t.text("altNames").nullable(); + t.specificType("keyUsages", "text[]").nullable(); + t.specificType("extendedKeyUsages", "text[]").nullable(); + t.datetime("notBefore").nullable(); + t.datetime("notAfter").nullable(); + t.string("keyAlgorithm").nullable(); + t.string("signatureAlgorithm").nullable(); + t.text("errorMessage").nullable(); + t.text("metadata").nullable(); + + t.index(["projectId"]); + t.index(["status"]); + t.index(["profileId"]); + t.index(["caId"]); + t.index(["certificateId"]); + t.index(["createdAt"]); + }); + } + + await createOnUpdateTrigger(knex, TableName.CertificateRequests); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.CertificateRequests); + await dropOnUpdateTrigger(knex, TableName.CertificateRequests); +} diff --git a/backend/src/db/migrations/20251128120000_add-pki-profile-external-configs.ts b/backend/src/db/migrations/20251128120000_add-pki-profile-external-configs.ts new file mode 100644 index 000000000..86ad4f5b4 --- /dev/null +++ b/backend/src/db/migrations/20251128120000_add-pki-profile-external-configs.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasExternalConfigs = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "externalConfigs"); + if (!hasExternalConfigs) { + await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { + t.text("externalConfigs").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasExternalConfigs = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "externalConfigs"); + if (hasExternalConfigs) { + await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => { + t.dropColumn("externalConfigs"); + }); + } +} diff --git a/backend/src/db/schemas/certificate-requests.ts b/backend/src/db/schemas/certificate-requests.ts new file mode 100644 index 000000000..e01e08bbd --- /dev/null +++ b/backend/src/db/schemas/certificate-requests.ts @@ -0,0 +1,34 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const CertificateRequestsSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + status: z.string(), + projectId: z.string(), + profileId: z.string().uuid().nullable().optional(), + caId: z.string().uuid().nullable().optional(), + certificateId: z.string().uuid().nullable().optional(), + csr: z.string().nullable().optional(), + commonName: z.string().nullable().optional(), + altNames: z.string().nullable().optional(), + keyUsages: z.string().array().nullable().optional(), + extendedKeyUsages: z.string().array().nullable().optional(), + notBefore: z.date().nullable().optional(), + notAfter: z.date().nullable().optional(), + keyAlgorithm: z.string().nullable().optional(), + signatureAlgorithm: z.string().nullable().optional(), + errorMessage: z.string().nullable().optional(), + metadata: z.string().nullable().optional() +}); + +export type TCertificateRequests = z.infer; +export type TCertificateRequestsInsert = Omit, TImmutableDBKeys>; +export type TCertificateRequestsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 78dcb1980..7db6e847d 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -16,6 +16,7 @@ export * from "./certificate-authority-certs"; export * from "./certificate-authority-crl"; export * from "./certificate-authority-secret"; export * from "./certificate-bodies"; +export * from "./certificate-requests"; export * from "./certificate-secrets"; export * from "./certificate-syncs"; export * from "./certificate-template-est-configs"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 444a6bd97..040d6e278 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -21,6 +21,7 @@ export enum TableName { CertificateAuthorityCrl = "certificate_authority_crl", Certificate = "certificates", CertificateBody = "certificate_bodies", + CertificateRequests = "certificate_requests", CertificateSecret = "certificate_secrets", CertificateTemplate = "certificate_templates", PkiCertificateTemplateV2 = "pki_certificate_templates_v2", diff --git a/backend/src/db/schemas/pki-certificate-profiles.ts b/backend/src/db/schemas/pki-certificate-profiles.ts index 04560bec6..0cf9cf160 100644 --- a/backend/src/db/schemas/pki-certificate-profiles.ts +++ b/backend/src/db/schemas/pki-certificate-profiles.ts @@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models"; export const PkiCertificateProfilesSchema = z.object({ id: z.string().uuid(), projectId: z.string(), - caId: z.string().uuid(), + caId: z.string().uuid().nullable().optional(), certificateTemplateId: z.string().uuid(), slug: z.string(), description: z.string().nullable().optional(), @@ -19,7 +19,9 @@ export const PkiCertificateProfilesSchema = z.object({ apiConfigId: z.string().uuid().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), - acmeConfigId: z.string().uuid().nullable().optional() + acmeConfigId: z.string().uuid().nullable().optional(), + issuerType: z.string().default("ca"), + externalConfigs: z.string().nullable().optional() }); export type TPkiCertificateProfiles = z.infer; 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/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 4b2608c24..6d97e3b9e 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -243,7 +243,7 @@ export const accessApprovalRequestServiceFactory = ({ ); const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; - const projectPath = `/projects/secret-management/${project.id}`; + const projectPath = `/organizations/${project.orgId}/projects/secret-management/${project.id}`; const approvalPath = `${projectPath}/approval`; const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; @@ -399,7 +399,7 @@ export const accessApprovalRequestServiceFactory = ({ const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; const editorFullName = `${editedByUser.firstName} ${editedByUser.lastName}`; - const projectPath = `/projects/secret-management/${project.id}`; + const projectPath = `/organizations/${project.orgId}/projects/secret-management/${project.id}`; const approvalPath = `${projectPath}/approval`; const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; @@ -766,7 +766,7 @@ export const accessApprovalRequestServiceFactory = ({ .map((appUser) => appUser.email) .filter((email): email is string => !!email); - const approvalPath = `/projects/secret-management/${project.id}/approval`; + const approvalPath = `/organizations/${project.orgId}/projects/secret-management/${project.id}/approval`; const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; await notificationService.createUserNotifications( diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index ab5b126c6..36b49a37c 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -388,6 +388,9 @@ export enum EventType { GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle", UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config", DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config", + CREATE_CERTIFICATE_REQUEST = "create-certificate-request", + GET_CERTIFICATE_REQUEST = "get-certificate-request", + GET_CERTIFICATE_FROM_REQUEST = "get-certificate-from-request", ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration", ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", @@ -2787,6 +2790,7 @@ interface CreateCertificateProfile { name: string; projectId: string; enrollmentType: string; + issuerType: string; }; } @@ -2845,7 +2849,6 @@ interface OrderCertificateFromProfile { type: EventType.ORDER_CERTIFICATE_FROM_PROFILE; metadata: { certificateProfileId: string; - orderId: string; profileName: string; }; } @@ -4195,6 +4198,31 @@ interface DisableCertificateRenewalConfigEvent { }; } +interface CreateCertificateRequestEvent { + type: EventType.CREATE_CERTIFICATE_REQUEST; + metadata: { + certificateRequestId: string; + profileId?: string; + caId?: string; + commonName?: string; + }; +} + +interface GetCertificateRequestEvent { + type: EventType.GET_CERTIFICATE_REQUEST; + metadata: { + certificateRequestId: string; + }; +} + +interface GetCertificateFromRequestEvent { + type: EventType.GET_CERTIFICATE_FROM_REQUEST; + metadata: { + certificateRequestId: string; + certificateId?: string; + }; +} + export type Event = | CreateSubOrganizationEvent | UpdateSubOrganizationEvent @@ -4574,6 +4602,9 @@ export type Event = | PamResourceDeleteEvent | UpdateCertificateRenewalConfigEvent | DisableCertificateRenewalConfigEvent + | CreateCertificateRequestEvent + | GetCertificateRequestEvent + | GetCertificateFromRequestEvent | AutomatedRenewCertificate | AutomatedRenewCertificateFailed | UserLoginEvent 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/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 3bbd58831..e34f9273f 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -450,8 +450,8 @@ export const licenseServiceFactory = ({ } = await licenseServerCloudApi.request.post( `/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`, { - success_url: `${envConfig.SITE_URL}/organization/billing`, - cancel_url: `${envConfig.SITE_URL}/organization/billing` + success_url: `${envConfig.SITE_URL}/organizations/${orgId}/billing`, + cancel_url: `${envConfig.SITE_URL}/organizations/${orgId}/billing` } ); @@ -464,7 +464,7 @@ export const licenseServiceFactory = ({ } = await licenseServerCloudApi.request.post( `/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`, { - return_url: `${envConfig.SITE_URL}/organization/billing` + return_url: `${envConfig.SITE_URL}/organizations/${orgId}/billing` } ); diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 81814a67c..8c45ed8f8 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -44,7 +44,6 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.Settings, ProjectPermissionSub.Environments, ProjectPermissionSub.Tags, - ProjectPermissionSub.AuditLogs, ProjectPermissionSub.IpAllowList, ProjectPermissionSub.CertificateAuthorities, ProjectPermissionSub.PkiAlerts, @@ -67,6 +66,8 @@ const buildAdminPermissionRules = () => { ); }); + can([ProjectPermissionAuditLogsActions.Read], ProjectPermissionSub.AuditLogs); + can( [ ProjectPermissionPkiTemplateActions.Read, diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 7a3747fed..7379d7ece 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -1,3 +1,6 @@ +import axios, { AxiosError } from "axios"; + +import { TPkiAcmeChallenges } from "@app/db/schemas/pki-acme-challenges"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { isPrivateIp } from "@app/lib/ip/ipRange"; @@ -13,14 +16,14 @@ import { import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; -type FetchError = Error & { - code?: string; -}; - type TPkiAcmeChallengeServiceFactoryDep = { acmeChallengeDAL: Pick< TPkiAcmeChallengeDALFactory, - "transaction" | "findByIdForChallengeValidation" | "markAsValidCascadeById" | "markAsInvalidCascadeById" + | "transaction" + | "findByIdForChallengeValidation" + | "markAsValidCascadeById" + | "markAsInvalidCascadeById" + | "updateById" >; }; @@ -28,9 +31,8 @@ export const pkiAcmeChallengeServiceFactory = ({ acmeChallengeDAL }: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => { const appCfg = getConfig(); - - const validateChallengeResponse = async (challengeId: string): Promise => { - const error: Error | undefined = await acmeChallengeDAL.transaction(async (tx) => { + const markChallengeAsReady = async (challengeId: string): Promise => { + return acmeChallengeDAL.transaction(async (tx) => { logger.info({ challengeId }, "Validating ACME challenge response"); const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); if (!challenge) { @@ -54,89 +56,102 @@ export const pkiAcmeChallengeServiceFactory = ({ if (challenge.type !== AcmeChallengeType.HTTP_01) { throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" }); } - let host = challenge.auth.identifierValue; + const host = challenge.auth.identifierValue; // check if host is a private ip address if (isPrivateIp(host)) { throw new BadRequestError({ message: "Private IP addresses are not allowed" }); } - if (appCfg.isAcmeDevelopmentMode && appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]) { - host = appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]; - logger.warn( - { srcHost: challenge.auth.identifierValue, dstHost: host }, - "Using ACME development HTTP-01 challenge host override" - ); - } - const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, `http://${host}`); - logger.info({ challengeUrl }, "Performing ACME HTTP-01 challenge validation"); - try { - // TODO: read config from the profile to get the timeout instead - const timeoutMs = 10 * 1000; // 10 seconds - // Notice: well, we are in a transaction, ideally we should not hold transaction and perform - // a long running operation for long time. But assuming we are not performing a tons of - // challenge validation at the same time, it should be fine. - const challengeResponse = await fetch(challengeUrl, { - // In case if we override the host in the development mode, still provide the original host in the header - // to help the upstream server to validate the request - headers: { Host: host }, - signal: AbortSignal.timeout(timeoutMs) - }); - if (challengeResponse.status !== 200) { - throw new AcmeIncorrectResponseError({ - message: `ACME challenge response is not 200: ${challengeResponse.status}` - }); - } - const challengeResponseBody = await challengeResponse.text(); - const thumbprint = challenge.auth.account.publicKeyThumbprint; - const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`; - if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) { - throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" }); - } - await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx); - } catch (exp) { - // TODO: we should retry the challenge validation a few times, but let's keep it simple for now - await acmeChallengeDAL.markAsInvalidCascadeById(challengeId, tx); - // Properly type and inspect the error - if (exp instanceof TypeError && exp.message.includes("fetch failed")) { - const { cause } = exp; - let errors: Error[] = []; - if (cause instanceof AggregateError) { - errors = cause.errors as Error[]; - } else if (cause instanceof Error) { - errors = [cause]; - } - // eslint-disable-next-line no-unreachable-loop - for (const err of errors) { - // TODO: handle multiple errors, return a compound error instead of just the first error - const fetchError = err as FetchError; - if (fetchError.code === "ECONNREFUSED" || fetchError.message.includes("ECONNREFUSED")) { - return new AcmeConnectionError({ message: "Connection refused" }); - } - if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) { - return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); - } - logger.error(exp, "Unknown error validating ACME challenge response"); - return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); - } - } else if (exp instanceof DOMException) { - if (exp.name === "TimeoutError") { - logger.error(exp, "Connection timed out while validating ACME challenge response"); - return new AcmeConnectionError({ message: "Connection timed out" }); - } - logger.error(exp, "Unknown error validating ACME challenge response"); - return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); - } else if (exp instanceof Error) { - logger.error(exp, "Error validating ACME challenge response"); - } else { - logger.error(exp, "Unknown error validating ACME challenge response"); - return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); - } - return exp; - } + return acmeChallengeDAL.updateById(challengeId, { status: AcmeChallengeStatus.Processing }, tx); }); - if (error) { - throw error; + }; + + const validateChallengeResponse = async (challengeId: string, retryCount: number): Promise => { + logger.info({ challengeId, retryCount }, "Validating ACME challenge response"); + const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId); + if (!challenge) { + throw new NotFoundError({ message: "ACME challenge not found" }); + } + if (challenge.status !== AcmeChallengeStatus.Processing) { + throw new BadRequestError({ + message: `ACME challenge is ${challenge.status} instead of ${AcmeChallengeStatus.Processing}` + }); + } + let host = challenge.auth.identifierValue; + if (appCfg.isAcmeDevelopmentMode && appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]) { + host = appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]; + logger.warn( + { srcHost: challenge.auth.identifierValue, dstHost: host }, + "Using ACME development HTTP-01 challenge host override" + ); + } + const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, `http://${host}`); + logger.info({ challengeUrl }, "Performing ACME HTTP-01 challenge validation"); + try { + // TODO: read config from the profile to get the timeout instead + const timeoutMs = 10 * 1000; // 10 seconds + // Notice: well, we are in a transaction, ideally we should not hold transaction and perform + // a long running operation for long time. But assuming we are not performing a tons of + // challenge validation at the same time, it should be fine. + const challengeResponse = await axios.get(challengeUrl.toString(), { + // In case if we override the host in the development mode, still provide the original host in the header + // to help the upstream server to validate the request + headers: { Host: challenge.auth.identifierValue }, + timeout: timeoutMs, + responseType: "text", + validateStatus: () => true + }); + if (challengeResponse.status !== 200) { + throw new AcmeIncorrectResponseError({ + message: `ACME challenge response is not 200: ${challengeResponse.status}` + }); + } + const challengeResponseBody: string = challengeResponse.data; + const thumbprint = challenge.auth.account.publicKeyThumbprint; + const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`; + if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) { + throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" }); + } + logger.info({ challengeId }, "ACME challenge response is correct, marking challenge as valid"); + await acmeChallengeDAL.markAsValidCascadeById(challengeId); + } catch (exp) { + if (retryCount >= 2) { + logger.error( + exp, + `Last attempt to validate ACME challenge response failed, marking ${challengeId} challenge as invalid` + ); + // This is the last attempt to validate the challenge response, if it fails, we mark the challenge as invalid + await acmeChallengeDAL.markAsInvalidCascadeById(challengeId); + } + // Properly type and inspect the error + if (axios.isAxiosError(exp)) { + const axiosError = exp as AxiosError; + const errorCode = axiosError.code; + const errorMessage = axiosError.message; + + if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) { + throw new AcmeConnectionError({ message: "Connection refused" }); + } + if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) { + throw new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); + } + if (errorCode === "ECONNRESET" || errorMessage.includes("ECONNRESET")) { + throw new AcmeConnectionError({ message: "Connection reset by peer" }); + } + if (errorCode === "ECONNABORTED" || errorMessage.includes("timeout")) { + logger.error(exp, "Connection timed out while validating ACME challenge response"); + throw new AcmeConnectionError({ message: "Connection timed out" }); + } + logger.error(exp, "Unknown error validating ACME challenge response"); + throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); + } + if (exp instanceof Error) { + logger.error(exp, "Error validating ACME challenge response"); + throw exp; + } + logger.error(exp, "Unknown error validating ACME challenge response"); + throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); } }; - return { validateChallengeResponse }; + return { markChallengeAsReady, validateChallengeResponse }; }; 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-queue.ts b/backend/src/ee/services/pki-acme/pki-acme-queue.ts new file mode 100644 index 000000000..851159981 --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-queue.ts @@ -0,0 +1,67 @@ +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; + +type TPkiAcmeQueueServiceFactoryDep = { + queueService: TQueueServiceFactory; + acmeChallengeService: TPkiAcmeChallengeServiceFactory; +}; + +export type TPkiAcmeQueueServiceFactory = Awaited>; + +export const pkiAcmeQueueServiceFactory = async ({ + queueService, + acmeChallengeService +}: TPkiAcmeQueueServiceFactoryDep) => { + const appCfg = getConfig(); + + // Initialize the worker to process challenge validation jobs + await queueService.startPg( + QueueJobs.PkiAcmeChallengeValidation, + async ([job]) => { + const { challengeId } = job.data; + const retryCount = job.retryCount || 0; + try { + logger.info({ challengeId, retryCount }, "Processing ACME challenge validation job"); + await acmeChallengeService.validateChallengeResponse(challengeId, retryCount); + logger.info({ challengeId, retryCount }, "ACME challenge validation completed successfully"); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error( + error, + `Failed to validate ACME challenge ${challengeId} (retryCount ${retryCount}): ${errorMessage}` + ); + // Re-throw to let pg-boss handle retries with exponential backoff + throw error; + } + }, + { + batchSize: 1, + workerCount: 2, + pollingIntervalSeconds: 1 + } + ); + + const queueChallengeValidation = async (challengeId: string): Promise => { + if (appCfg.isSecondaryInstance) { + return; + } + + logger.info({ challengeId }, "Queueing ACME challenge validation"); + await queueService.queuePg( + QueueJobs.PkiAcmeChallengeValidation, + { challengeId }, + { + retryLimit: 3, + retryDelay: 30, // Base delay of 30 seconds + retryBackoff: true // Exponential backoff: 30s, 60s, 120s + } + ); + }; + + return { + queueChallengeValidation + }; +}; 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 4f560ade7..d9654e50b 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -31,12 +31,17 @@ import { orderCertificate } from "@app/services/certificate-authority/acme/acme- import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal"; -import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils"; +import { + extractAlgorithmsFromCSR, + extractCertificateRequestFromCSR +} from "@app/services/certificate-common/certificate-csr-utils"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { EnrollmentType, TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; +import { TCertificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal"; +import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -62,6 +67,7 @@ import { import { buildUrl, extractAccountIdFromKid, validateDnsIdentifier } from "./pki-acme-fns"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; +import { TPkiAcmeQueueServiceFactory } from "./pki-acme-queue"; import { AcmeAuthStatus, AcmeChallengeStatus, @@ -94,12 +100,13 @@ import { type TPkiAcmeServiceFactoryDep = { projectDAL: Pick; appConnectionDAL: Pick; - certificateDAL: Pick; + certificateDAL: Pick; certificateAuthorityDAL: Pick; externalCertificateAuthorityDAL: Pick; certificateProfileDAL: Pick; certificateBodyDAL: Pick; certificateSecretDAL: Pick; + certificateTemplateV2DAL: Pick; acmeAccountDAL: Pick< TPkiAcmeAccountDALFactory, "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" @@ -126,7 +133,9 @@ type TPkiAcmeServiceFactoryDep = { >; licenseService: Pick; certificateV3Service: Pick; - acmeChallengeService: TPkiAcmeChallengeServiceFactory; + certificateTemplateV2Service: Pick; + acmeChallengeService: Pick; + pkiAcmeQueueService: Pick; }; export const pkiAcmeServiceFactory = ({ @@ -138,6 +147,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL, certificateBodyDAL, certificateSecretDAL, + certificateTemplateV2DAL, acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, @@ -147,7 +157,9 @@ export const pkiAcmeServiceFactory = ({ kmsService, licenseService, certificateV3Service, - acmeChallengeService + certificateTemplateV2Service, + acmeChallengeService, + pkiAcmeQueueService }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findByIdWithConfigs(profileId); @@ -683,6 +695,13 @@ export const pkiAcmeServiceFactory = ({ payload: TFinalizeAcmeOrderPayload; }): Promise> => { const profile = (await certificateProfileDAL.findByIdWithConfigs(profileId))!; + + if (!profile.caId) { + throw new BadRequestError({ + message: "Self-signed certificates are not supported for ACME enrollment" + }); + } + let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); if (!order) { throw new NotFoundError({ message: "ACME order not found" }); @@ -703,9 +722,6 @@ export const pkiAcmeServiceFactory = ({ // Check and validate the CSR const certificateRequest = extractCertificateRequestFromCSR(csr); - if (!certificateRequest.commonName) { - throw new AcmeBadCSRError({ message: "Invalid CSR: Common name is required" }); - } if ( certificateRequest.subjectAlternativeNames?.some( (san) => san.type !== CertSubjectAlternativeNameType.DNS_NAME @@ -721,7 +737,7 @@ export const pkiAcmeServiceFactory = ({ const csrIdentifierValues = new Set( (certificateRequest.subjectAlternativeNames ?? []) .map((san) => san.value.toLowerCase()) - .concat([certificateRequest.commonName.toLowerCase()]) + .concat(certificateRequest.commonName ? [certificateRequest.commonName.toLowerCase()] : []) ); if ( csrIdentifierValues.size !== orderWithAuthorizations.authorizations.length || @@ -732,7 +748,7 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeBadCSRError({ message: "Invalid CSR: Common name + SANs mismatch with order identifiers" }); } - const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId!); if (!ca) { throw new NotFoundError({ message: "Certificate Authority not found" }); } @@ -765,14 +781,39 @@ export const pkiAcmeServiceFactory = ({ const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!; const csrObj = new x509.Pkcs10CertificateRequest(csr); const csrPem = csrObj.toString("pem"); - // TODO: for internal CA, we rely on the internal certificate authority service to check CSR against the template - // we should check the CSR against the template here + + const { keyAlgorithm: extractedKeyAlgorithm, signatureAlgorithm: extractedSignatureAlgorithm } = + extractAlgorithmsFromCSR(csr); + + certificateRequest.keyAlgorithm = extractedKeyAlgorithm; + certificateRequest.signatureAlgorithm = extractedSignatureAlgorithm; + if (finalizingOrder.notAfter) { + const notBefore = finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : new Date(); + const notAfter = new Date(finalizingOrder.notAfter); + const diffMs = notAfter.getTime() - notBefore.getTime(); + const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24)); + certificateRequest.validity = { ttl: `${diffDays}d` }; + } + + const template = await certificateTemplateV2DAL.findById(profile.certificateTemplateId); + if (!template) { + throw new NotFoundError({ message: "Certificate template not found" }); + } + const validationResult = await certificateTemplateV2Service.validateCertificateRequest( + template.id, + certificateRequest + ); + if (!validationResult.isValid) { + throw new AcmeBadCSRError({ message: `Invalid CSR: ${validationResult.errors.join(", ")}` }); + } // TODO: this is pretty slow, and we are holding the transaction open for a long time, // we should queue the certificate issuance to a background job instead const cert = await orderCertificate( { caId: certificateAuthority!.id, - commonName: certificateRequest.commonName!, + // It is possible that the CSR does not have a common name, in which case we use an empty string + // (more likely than not for a CSR from a modern ACME client like certbot, cert-manager, etc.) + commonName: certificateRequest.commonName ?? "", altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value), csr: Buffer.from(csrPem), // TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now @@ -815,6 +856,8 @@ export const pkiAcmeServiceFactory = ({ // TODO: audit log the error if (exp instanceof BadRequestError) { errorToReturn = new AcmeBadCSRError({ message: `Invalid CSR: ${exp.message}` }); + } else if (exp instanceof AcmeError) { + errorToReturn = exp; } else { errorToReturn = new AcmeServerInternalError({ message: "Failed to sign certificate with internal error" }); } @@ -969,7 +1012,8 @@ export const pkiAcmeServiceFactory = ({ if (!result) { throw new NotFoundError({ message: "ACME challenge not found" }); } - await acmeChallengeService.validateChallengeResponse(challengeId); + await acmeChallengeService.markChallengeAsReady(challengeId); + await pkiAcmeQueueService.queueChallengeValidation(challengeId); const challenge = (await acmeChallengeDAL.findByIdForChallengeValidation(challengeId))!; return { status: 200, diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 3ddb424f1..6607ce711 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -1,6 +1,8 @@ import { JWSHeaderParameters } from "jose"; import { z } from "zod"; +import { TPkiAcmeChallenges } from "@app/db/schemas/pki-acme-challenges"; + import { AcmeOrderResourceSchema, CreateAcmeAccountBodySchema, @@ -176,5 +178,6 @@ export type TPkiAcmeServiceFactory = { }; export type TPkiAcmeChallengeServiceFactory = { - validateChallengeResponse: (challengeId: string) => Promise; + markChallengeAsReady: (challengeId: string) => Promise; + validateChallengeResponse: (challengeId: string, retryCount: number) => Promise; }; 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/ee/services/secret-approval-request/secret-approval-request-fns.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts index 8f1c3d060..69d36e66f 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts @@ -37,7 +37,7 @@ export const sendApprovalEmailsFn = async ({ type: NotificationType.SECRET_CHANGE_REQUEST, title: "Secret Change Request", body: `You have a new secret change request pending your review for the project **${project.name}** in the organization **${project.organization.name}**.`, - link: `/projects/secret-management/${project.id}/approval` + link: `/organizations/${project.orgId}/projects/secret-management/${project.id}/approval` })) ); @@ -51,7 +51,7 @@ export const sendApprovalEmailsFn = async ({ firstName: reviewerUser.firstName, projectName: project.name, organizationName: project.organization.name, - approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval}` + approvalUrl: `${cfg.SITE_URL}/organizations/${project.orgId}/projects/secret-management/${project.id}/approval}` }, template: SmtpTemplates.SecretApprovalRequestNeedsReview }); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index e6455c113..6b0f7e0ed 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -1037,7 +1037,7 @@ export const secretApprovalRequestServiceFactory = ({ bypassReason, secretPath: policy.secretPath, environment: env.name, - approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval` + approvalUrl: `${cfg.SITE_URL}/organizations/${project.orgId}/projects/secret-management/${project.id}/approval` }, template: SmtpTemplates.AccessSecretRequestBypassed }); @@ -1416,7 +1416,7 @@ export const secretApprovalRequestServiceFactory = ({ const env = await projectEnvDAL.findOne({ id: policy.envId }); const user = await userDAL.findById(actorId); - const projectPath = `/projects/secret-management/${projectId}`; + const projectPath = `/organizations/${actorOrgId}/projects/secret-management/${projectId}`; const approvalPath = `${projectPath}/approval`; const cfg = getConfig(); const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; @@ -1792,7 +1792,7 @@ export const secretApprovalRequestServiceFactory = ({ const user = await userDAL.findById(actorId); const env = await projectEnvDAL.findOne({ id: policy.envId }); - const projectPath = `/projects/secret-management/${project.id}`; + const projectPath = `/organizations/${actorOrgId}/projects/secret-management/${project.id}`; const approvalPath = `${projectPath}/approval`; const cfg = getConfig(); const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts index f653802b6..3c902e112 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts @@ -156,7 +156,7 @@ export const secretRotationV2QueueServiceFactory = async ({ const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation]; - const rotationPath = `/projects/secret-management/${projectId}/secrets/${environment.slug}`; + const rotationPath = `/organizations/${project.orgId}/projects/secret-management/${projectId}/secrets/${environment.slug}`; await notificationService.createUserNotifications( projectAdmins.map((admin) => ({ diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts index 406c25e03..2b6ab6a20 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts @@ -637,7 +637,7 @@ export const secretScanningV2QueueServiceFactory = async ({ numberOfSecrets: payload.numberOfSecrets, isDiffScan: payload.isDiffScan, url: encodeURI( - `${appCfg.SITE_URL}/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}` + `${appCfg.SITE_URL}/organizations/${project.orgId}/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}` ), timestamp } @@ -648,7 +648,7 @@ export const secretScanningV2QueueServiceFactory = async ({ timestamp, errorMessage: payload.errorMessage, url: encodeURI( - `${appCfg.SITE_URL}/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}` + `${appCfg.SITE_URL}/organizations/${project.orgId}/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}` ) } }); diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index d70a7d4ac..99fb1a63a 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 8cf8555f9..c46e9c023 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -61,8 +61,10 @@ 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", SecretReplication = "secret-replication", SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication PkiSync = "pki-sync", @@ -80,7 +82,8 @@ export enum QueueName { UserNotification = "user-notification", HealthAlert = "health-alert", CertificateV3AutoRenewal = "certificate-v3-auto-renewal", - PamAccountRotation = "pam-account-rotation" + PamAccountRotation = "pam-account-rotation", + PkiAcmeChallengeValidation = "pki-acme-challenge-validation" } export enum QueueJobs { @@ -120,11 +123,13 @@ 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", SecretScanningV2SendNotification = "secret-scanning-v2-notification", CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber", + CaIssueCertificateFromProfile = "ca-issue-certificate-from-profile", PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal", TelemetryAggregatedEvents = "telemetry-aggregated-events", DailyReminders = "daily-reminders", @@ -132,7 +137,8 @@ export enum QueueJobs { UserNotification = "user-notification-job", HealthAlert = "health-alert", CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal", - PamAccountRotation = "pam-account-rotation" + PamAccountRotation = "pam-account-rotation", + PkiAcmeChallengeValidation = "pki-acme-challenge-validation" } export type TQueueJobTypes = { @@ -219,11 +225,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; }; } | { @@ -343,6 +357,21 @@ export type TQueueJobTypes = { caType: CaType; }; }; + [QueueName.CertificateIssuance]: { + name: QueueJobs.CaIssueCertificateFromProfile; + payload: { + certificateId: string; + profileId: string; + caId: string; + commonName?: string; + altNames?: string[]; + ttl: string; + signatureAlgorithm: string; + keyAlgorithm: string; + keyUsages?: string[]; + extendedKeyUsages?: string[]; + }; + }; [QueueName.DailyReminders]: { name: QueueJobs.DailyReminders; payload: undefined; @@ -375,6 +404,10 @@ export type TQueueJobTypes = { name: QueueJobs.PamAccountRotation; payload: undefined; }; + [QueueName.PkiAcmeChallengeValidation]: { + name: QueueJobs.PkiAcmeChallengeValidation; + payload: { challengeId: string }; + }; }; const SECRET_SCANNING_JOBS = [ 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 2b2023eb2..914491d3c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -81,6 +81,7 @@ import { pkiAcmeChallengeDALFactory } from "@app/ee/services/pki-acme/pki-acme-c import { pkiAcmeChallengeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-challenge-service"; import { pkiAcmeOrderAuthDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-auth-dal"; import { pkiAcmeOrderDALFactory } from "@app/ee/services/pki-acme/pki-acme-order-dal"; +import { pkiAcmeQueueServiceFactory } from "@app/ee/services/pki-acme/pki-acme-queue"; import { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; @@ -173,6 +174,7 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue"; import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; +import { certificateIssuanceQueueFactory } from "@app/services/certificate-authority/certificate-issuance-queue"; import { externalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal"; import { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-dal"; import { InternalCertificateAuthorityFns } from "@app/services/certificate-authority/internal/internal-certificate-authority-fns"; @@ -180,6 +182,8 @@ import { internalCertificateAuthorityServiceFactory } from "@app/services/certif import { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service"; import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service"; +import { certificateRequestDALFactory } from "@app/services/certificate-request/certificate-request-dal"; +import { certificateRequestServiceFactory } from "@app/services/certificate-request/certificate-request-service"; import { certificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; @@ -1092,6 +1096,7 @@ export const registerRoutes = async ( const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); const certificateSecretDAL = certificateSecretDALFactory(db); + const certificateRequestDAL = certificateRequestDALFactory(db); const certificateSyncDAL = certificateSyncDALFactory(db); const pkiAlertDAL = pkiAlertDALFactory(db); @@ -1187,7 +1192,7 @@ export const registerRoutes = async ( certificateBodyDAL, certificateSecretDAL, certificateAuthorityDAL, - certificateAuthorityCertDAL, + externalCertificateAuthorityDAL, permissionService, licenseService, kmsService, @@ -1329,7 +1334,8 @@ export const registerRoutes = async ( eventBusService, licenseService, membershipRoleDAL, - membershipUserDAL + membershipUserDAL, + telemetryService }); const projectService = projectServiceFactory({ @@ -1874,7 +1880,12 @@ export const registerRoutes = async ( dynamicSecretProviders, dynamicSecretDAL, folderDAL, - kmsService + kmsService, + smtpService, + userDAL, + identityDAL, + projectMembershipDAL, + projectDAL }); const dynamicSecretService = dynamicSecretServiceFactory({ projectDAL, @@ -1907,6 +1918,7 @@ export const registerRoutes = async ( // DAILY const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ + scimService, auditLogDAL, queueService, secretVersionDAL, @@ -2208,6 +2220,31 @@ export const registerRoutes = async ( pkiSyncQueue }); + const certificateRequestService = certificateRequestServiceFactory({ + certificateRequestDAL, + certificateDAL, + certificateService, + permissionService + }); + + const certificateIssuanceQueue = certificateIssuanceQueueFactory({ + certificateAuthorityDAL, + appConnectionDAL, + appConnectionService, + externalCertificateAuthorityDAL, + certificateDAL, + projectDAL, + kmsService, + certificateBodyDAL, + certificateSecretDAL, + queueService, + pkiSubscriberDAL, + pkiSyncDAL, + pkiSyncQueue, + certificateProfileDAL, + certificateRequestService + }); + const certificateV3Service = certificateV3ServiceFactory({ certificateDAL, certificateSecretDAL, @@ -2219,7 +2256,12 @@ export const registerRoutes = async ( permissionService, certificateSyncDAL, pkiSyncDAL, - pkiSyncQueue + pkiSyncQueue, + kmsService, + projectDAL, + certificateBodyDAL, + certificateIssuanceQueue, + certificateRequestService }); const certificateV3Queue = certificateV3QueueServiceFactory({ @@ -2244,6 +2286,12 @@ export const registerRoutes = async ( const acmeChallengeService = pkiAcmeChallengeServiceFactory({ acmeChallengeDAL }); + + const pkiAcmeQueueService = await pkiAcmeQueueServiceFactory({ + queueService, + acmeChallengeService + }); + const pkiAcmeService = pkiAcmeServiceFactory({ projectDAL, appConnectionDAL, @@ -2253,6 +2301,7 @@ export const registerRoutes = async ( certificateProfileDAL, certificateBodyDAL, certificateSecretDAL, + certificateTemplateV2DAL, acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, @@ -2262,7 +2311,9 @@ export const registerRoutes = async ( kmsService, licenseService, certificateV3Service, - acmeChallengeService + certificateTemplateV2Service, + acmeChallengeService, + pkiAcmeQueueService }); const pkiSubscriberService = pkiSubscriberServiceFactory({ @@ -2445,6 +2496,7 @@ export const registerRoutes = async ( await pkiSubscriberQueue.startDailyAutoRenewalJob(); await pkiAlertV2Queue.init(); await certificateV3Queue.init(); + await certificateIssuanceQueue.initializeCertificateIssuanceQueue(); await microsoftTeamsService.start(); await dynamicSecretQueueService.init(); await eventBusService.init(); @@ -2510,6 +2562,7 @@ export const registerRoutes = async ( auditLogStream: auditLogStreamService, certificate: certificateService, certificateV3: certificateV3Service, + certificateRequest: certificateRequestService, certificateEstV3: certificateEstV3Service, sshCertificateAuthority: sshCertificateAuthorityService, sshCertificateTemplate: sshCertificateTemplateService, 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 a459adf66..072abbadb 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"; @@ -175,7 +179,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedRedisConnectionSchema.options, ...SanitizedMongoDBConnectionSchema.options, ...SanitizedLaravelForgeConnectionSchema.options, - ...SanitizedChefConnectionSchema.options + ...SanitizedChefConnectionSchema.options, + ...SanitizedDNSMadeEasyConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -221,7 +226,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ RedisConnectionListItemSchema, MongoDBConnectionListItemSchema, 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 1c3eade58..3c04dbb48 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"; @@ -79,6 +80,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-profiles-router.ts b/backend/src/server/routes/v1/certificate-profiles-router.ts index 5792c5e83..ff770326d 100644 --- a/backend/src/server/routes/v1/certificate-profiles-router.ts +++ b/backend/src/server/routes/v1/certificate-profiles-router.ts @@ -8,7 +8,8 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { CertStatus } from "@app/services/certificate/certificate-types"; -import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; +import { ExternalConfigUnionSchema } from "@app/services/certificate-profile/certificate-profile-external-config-schemas"; +import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types"; export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => { server.route({ @@ -23,7 +24,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid body: z .object({ projectId: z.string().min(1), - caId: z.string().uuid(), + caId: z.string().uuid().optional(), certificateTemplateId: z.string().uuid(), slug: z .string() @@ -32,6 +33,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid .regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"), description: z.string().max(1000).optional(), enrollmentType: z.nativeEnum(EnrollmentType), + issuerType: z.nativeEnum(IssuerType).default(IssuerType.CA), estConfig: z .object({ disableBootstrapCaValidation: z.boolean().default(false), @@ -45,53 +47,113 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid renewBeforeDays: z.number().min(1).max(30).optional() }) .optional(), - acmeConfig: z.object({}).optional() + acmeConfig: z.object({}).optional(), + externalConfigs: ExternalConfigUnionSchema }) .refine( (data) => { if (data.enrollmentType === EnrollmentType.EST) { - if (!data.estConfig) { - return false; - } - if (data.apiConfig) { - return false; - } - if (data.acmeConfig) { - return false; - } - } - if (data.enrollmentType === EnrollmentType.API) { - if (!data.apiConfig) { - return false; - } - if (data.estConfig) { - return false; - } - if (data.acmeConfig) { - return false; - } - } - if (data.enrollmentType === EnrollmentType.ACME) { - if (!data.acmeConfig) { - return false; - } - if (data.estConfig) { - return false; - } - if (data.apiConfig) { - return false; - } + return !!data.estConfig; } return true; }, { - message: - "EST enrollment type requires EST configuration and cannot have API or ACME configuration. API enrollment type requires API configuration and cannot have EST or ACME configuration. ACME enrollment type requires ACME configuration and cannot have EST or API configuration." + message: "EST enrollment type requires EST configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.API) { + return !!data.apiConfig; + } + return true; + }, + { + message: "API enrollment type requires API configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.ACME) { + return !!data.acmeConfig; + } + return true; + }, + { + message: "ACME enrollment type requires ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.EST) { + return !data.apiConfig && !data.acmeConfig; + } + return true; + }, + { + message: "EST enrollment type cannot have API or ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.API) { + return !data.estConfig && !data.acmeConfig; + } + return true; + }, + { + message: "API enrollment type cannot have EST or ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.ACME) { + return !data.estConfig && !data.apiConfig; + } + return true; + }, + { + message: "ACME enrollment type cannot have EST or API configuration" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.CA) { + return !!data.caId; + } + return true; + }, + { + message: "CA issuer type requires a CA ID" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.SELF_SIGNED) { + return !data.caId; + } + return true; + }, + { + message: "Self-signed issuer type cannot have a CA ID" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.SELF_SIGNED) { + return data.enrollmentType === EnrollmentType.API; + } + return true; + }, + { + message: "Self-signed issuer type only supports API enrollment" } ), response: { 200: z.object({ - certificateProfile: PkiCertificateProfilesSchema + certificateProfile: PkiCertificateProfilesSchema.extend({ + externalConfigs: ExternalConfigUnionSchema + }) }) } }, @@ -115,7 +177,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid certificateProfileId: certificateProfile.id, name: certificateProfile.slug, projectId: certificateProfile.projectId, - enrollmentType: certificateProfile.enrollmentType + enrollmentType: certificateProfile.enrollmentType, + issuerType: certificateProfile.issuerType } } }); @@ -139,11 +202,21 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid limit: z.coerce.number().min(1).max(100).default(20), search: z.string().optional(), enrollmentType: z.nativeEnum(EnrollmentType).optional(), + issuerType: z.nativeEnum(IssuerType).optional(), caId: z.string().uuid().optional() }), response: { 200: z.object({ certificateProfiles: PkiCertificateProfilesSchema.extend({ + certificateAuthority: z + .object({ + id: z.string(), + status: z.string(), + name: z.string(), + isExternal: z.boolean().optional(), + externalType: z.string().nullable().optional() + }) + .optional(), metrics: z .object({ profileId: z.string(), @@ -174,7 +247,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid id: z.string(), directoryUrl: z.string() }) - .optional() + .optional(), + externalConfigs: ExternalConfigUnionSchema }).array(), totalCount: z.number() }) @@ -220,12 +294,16 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid response: { 200: z.object({ certificateProfile: PkiCertificateProfilesSchema.extend({ + externalConfigs: ExternalConfigUnionSchema + }).extend({ certificateAuthority: z .object({ id: z.string(), projectId: z.string(), status: z.string(), - name: z.string() + name: z.string(), + isExternal: z.boolean().optional(), + externalType: z.string().nullable().optional() }) .optional(), certificateTemplate: z @@ -250,7 +328,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid autoRenew: z.boolean(), renewBeforeDays: z.number().optional() }) - .optional() + .optional(), + externalConfigs: ExternalConfigUnionSchema }) }) } @@ -298,7 +377,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid }), response: { 200: z.object({ - certificateProfile: PkiCertificateProfilesSchema + certificateProfile: PkiCertificateProfilesSchema.extend({ + externalConfigs: ExternalConfigUnionSchema + }) }) } }, @@ -339,6 +420,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid .optional(), description: z.string().max(1000).optional(), enrollmentType: z.nativeEnum(EnrollmentType).optional(), + issuerType: z.nativeEnum(IssuerType).optional(), estConfig: z .object({ disableBootstrapCaValidation: z.boolean().default(false), @@ -351,7 +433,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid autoRenew: z.boolean().default(false), renewBeforeDays: z.number().min(1).max(30).optional() }) - .optional() + .optional(), + externalConfigs: ExternalConfigUnionSchema }) .refine( (data) => { @@ -373,7 +456,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid ), response: { 200: z.object({ - certificateProfile: PkiCertificateProfilesSchema + certificateProfile: PkiCertificateProfilesSchema.extend({ + externalConfigs: ExternalConfigUnionSchema + }) }) } }, @@ -418,7 +503,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid }), response: { 200: z.object({ - certificateProfile: PkiCertificateProfilesSchema + certificateProfile: PkiCertificateProfilesSchema.extend({ + externalConfigs: ExternalConfigUnionSchema + }) }) } }, diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index e8cdbb540..f14ff437f 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -4,24 +4,794 @@ 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 { NotFoundError } from "@app/lib/errors"; 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 { CertKeyAlgorithm, CertSignatureAlgorithm, CrlReason } from "@app/services/certificate/certificate-types"; +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; import { - validateAltNamesField, - validateCaDateField -} from "@app/services/certificate-authority/certificate-authority-validators"; -import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; + 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 { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types"; +import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; +import { TCertificateFromProfileResponse } from "@app/services/certificate-v3/certificate-v3-types"; -export const registerCertRouter = async (server: FastifyZodProvider) => { +import { booleanSchema } from "../sanitizedSchemas"; + +type CertificateServiceResponse = TCertificateFromProfileResponse | Omit; + +const extractCertificateData = (data: CertificateServiceResponse) => ({ + certificate: data.certificate, + issuingCaCertificate: data.issuingCaCertificate, + certificateChain: data.certificateChain, + privateKey: "privateKey" in data ? data.privateKey : undefined, + serialNumber: data.serialNumber, + certificateId: data.certificateId +}); + +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: { + attributes?: { notBefore?: string; notAfter?: string; ttl?: string }; + notBefore?: string; + notAfter?: string; + ttl?: string; +}) => { + if (data.attributes) { + const hasDateFields = data.attributes.notBefore || data.attributes.notAfter; + const hasTtl = data.attributes.ttl; + return !(hasDateFields && hasTtl); + } + const hasDateFields = data.notBefore || data.notAfter; + const hasTtl = data.ttl; + return !(hasDateFields && hasTtl); +}; + +const validateDateOrder = (data: { + attributes?: { notBefore?: string; notAfter?: string }; + notBefore?: string; + notAfter?: string; +}) => { + if (data.attributes?.notBefore && data.attributes?.notAfter) { + const notBefore = new Date(data.attributes.notBefore); + const notAfter = new Date(data.attributes.notAfter); + return notBefore < notAfter; + } + 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: "/", + 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") + .optional(), + attributes: z + .object({ + commonName: validateTemplateRegexField.optional(), + keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(), + extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().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).optional(), + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional(), + 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() + }) + .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 + .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() + }) + .nullable(), + certificateRequestId: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { csr, attributes, ...requestBody } = req.body; + const profile = await server.services.certificateProfile.getProfileById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + profileId: requestBody.profileId + }); + + let useOrderFlow = false; + if (profile?.caId) { + const ca = await server.services.certificateAuthority.getCaById({ + caId: profile.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL; + useOrderFlow = caType !== CaType.INTERNAL; + } + + if (useOrderFlow) { + const certificateOrderObject = { + altNames: attributes?.altNames || [], + validity: { ttl: attributes?.ttl || "" }, + commonName: attributes?.commonName, + keyUsages: attributes?.keyUsages, + extendedKeyUsages: attributes?.extendedKeyUsages, + notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined, + notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined, + signatureAlgorithm: attributes?.signatureAlgorithm, + keyAlgorithm: attributes?.keyAlgorithm, + csr + }; + + const data = await server.services.certificateV3.orderCertificateFromProfile({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + profileId: requestBody.profileId, + certificateOrder: certificateOrderObject, + removeRootsFromChain: requestBody.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, + metadata: { + certificateProfileId: requestBody.profileId, + profileName: data.profileName + } + } + }); + + return { + certificate: null, + certificateRequestId: data.certificateRequestId + }; + } + + if (csr) { + const extractedCsrData = extractCertificateRequestFromCSR(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: requestBody.profileId, + csr, + validity: { ttl: attributes?.ttl || "" }, + notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined, + notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined, + enrollmentType: EnrollmentType.API, + removeRootsFromChain: requestBody.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.SIGN_CERTIFICATE_FROM_PROFILE, + metadata: { + certificateProfileId: requestBody.profileId, + certificateId: data.certificateId, + profileName: data.profileName, + commonName: extractedCsrData.commonName || "" + } + } + }); + return { + certificate: extractCertificateData(data), + certificateRequestId: data.certificateRequestId + }; + } + + const certificateRequestForService: CertificateRequestForService = { + commonName: attributes?.commonName, + keyUsages: attributes?.keyUsages, + extendedKeyUsages: attributes?.extendedKeyUsages, + altNames: attributes?.altNames, + validity: { ttl: attributes?.ttl || "" }, + notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined, + notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined, + signatureAlgorithm: attributes?.signatureAlgorithm, + keyAlgorithm: attributes?.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: requestBody.profileId, + certificateRequest: mappedCertificateRequest, + removeRootsFromChain: requestBody.removeRootsFromChain + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: data.projectId, + event: { + type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE, + metadata: { + certificateProfileId: requestBody.profileId, + certificateId: data.certificateId, + commonName: attributes?.commonName || "", + profileName: data.profileName + } + } + }); + return { + certificate: extractCertificateData(data), + certificateRequestId: data.certificateRequestId + }; + } + }); server.route({ method: "GET", - url: "/:serialNumber", + url: "/certificate-requests/:requestId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificates], + params: z.object({ + requestId: z.string().uuid() + }), + query: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + status: z.nativeEnum(CertificateRequestStatus), + certificate: z.string().nullable(), + privateKey: z.string().nullable(), + serialNumber: z.string().nullable(), + errorMessage: z.string().nullable(), + createdAt: z.date(), + updatedAt: z.date() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const data = await server.services.certificateRequest.getCertificateFromRequest({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: (req.query as { projectId: string }).projectId, + certificateRequestId: req.params.requestId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: (req.query as { projectId: string }).projectId, + event: { + type: EventType.GET_CERTIFICATE_REQUEST, + metadata: { + certificateRequestId: req.params.requestId + } + } + }); + return data; + } + }); + + server.route({ + method: "POST", + url: "/issue-certificate", + config: { + rateLimit: writeLimit + }, + schema: { + hide: true, + deprecated: true, + tags: [ApiDocsTags.PkiCertificates], + description: "This endpoint will be removed in a future version.", + 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(), + certificateRequestId: 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: true, + deprecated: true, + tags: [ApiDocsTags.PkiCertificates], + description: "This endpoint will be removed in a future version.", + 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(), + certificateRequestId: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + 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 + }); + + const certificateRequestData = extractCertificateRequestFromCSR(req.body.csr); + + 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: certificateRequestData.commonName || "" + } + } + }); + + return data; + } + }); + + server.route({ + method: "POST", + url: "/order-certificate", + config: { + rateLimit: writeLimit + }, + schema: { + hide: true, + deprecated: true, + tags: [ApiDocsTags.PkiCertificates], + description: "This endpoint will be removed in a future version.", + body: z + .object({ + profileId: z.string().uuid(), + subjectAlternativeNames: z.array( + z.object({ + type: z.nativeEnum(CertSubjectAlternativeNameType), + value: z + .string() + .trim() + .min(1, "SAN value cannot be empty") + .max(255, "SAN value must be less than 255 characters") + }) + ), + 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({ + certificate: z.string().optional(), + certificateRequestId: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateOrderObject = { + 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 + }; + + 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: certificateOrderObject, + 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, + 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(), + certificateRequestId: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const originalCertificate = await server.services.certificate.getCert({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.id + }); + if (!originalCertificate) { + throw new NotFoundError({ message: "Original certificate not found" }); + } + + 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" + }; + } + }); + + server.route({ + method: "GET", + url: "/:id", config: { rateLimit: readLimit }, @@ -31,7 +801,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 +811,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 +837,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 +849,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 +857,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 +883,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 +895,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 +909,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 +940,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 +1004,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 +1014,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 +1029,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 +1052,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { return { message: "Successfully revoked certificate", - serialNumber: req.params.serialNumber, + serialNumber: cert.serialNumber, revokedAt }; } @@ -520,7 +1060,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", - url: "/:serialNumber", + url: "/:id", config: { rateLimit: writeLimit }, @@ -530,7 +1070,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 +1080,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 +1108,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:serialNumber/certificate", + url: "/:id/certificate", config: { rateLimit: readLimit }, @@ -578,7 +1118,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 +1130,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 +1160,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:serialNumber/pkcs12", + url: "/:id/pkcs12", config: { rateLimit: writeLimit }, @@ -630,7 +1170,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 +1185,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 +1211,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/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index d7cd86330..71f299e43 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -408,6 +408,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } }); + // deprecated - use the GET /token-auth/tokens/:tokenId instead, this endpoint will be removed in the future server.route({ method: "GET", url: "/token-auth/identities/:identityId/tokens/:tokenId", @@ -416,7 +417,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - hide: false, + hide: true, tags: [ApiDocsTags.TokenAuth], description: "Get token for machine identity with Token Auth", security: [ @@ -436,13 +437,11 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider }, handler: async (req) => { const { token, identityMembershipOrg } = await server.services.identityTokenAuth.getTokenAuthTokenById({ - identityId: req.params.identityId, tokenId: req.params.tokenId, actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - isActorSuperAdmin: isSuperAdmin(req.auth) + actorAuthMethod: req.permission.authMethod }); await server.services.auditLog.createAuditLog({ @@ -462,6 +461,57 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } }); + server.route({ + method: "GET", + url: "/token-auth/tokens/:tokenId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.TokenAuth], + description: "Get token for machine identity with Token Auth", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + tokenId: z.string().describe(TOKEN_AUTH.GET_TOKEN.tokenId) + }), + response: { + 200: z.object({ + token: IdentityAccessTokensSchema + }) + } + }, + handler: async (req) => { + const { token, identityMembershipOrg } = await server.services.identityTokenAuth.getTokenAuthTokenById({ + tokenId: req.params.tokenId, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg.scopeOrgId, + event: { + type: EventType.GET_TOKEN_IDENTITY_TOKEN_AUTH, + metadata: { + identityId: identityMembershipOrg.identity.id, + identityName: identityMembershipOrg.identity.name, + tokenId: token.id + } + } + }); + + return { token }; + } + }); + server.route({ method: "PATCH", url: "/token-auth/tokens/:tokenId", 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 71% rename from backend/src/server/routes/v3/certificates-router.ts rename to backend/src/server/routes/v3/deprecated-certificates-router.ts index f13f77c34..ab8ae176c 100644 --- a/backend/src/server/routes/v3/certificates-router.ts +++ b/backend/src/server/routes/v3/deprecated-certificates-router.ts @@ -2,16 +2,12 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags } from "@app/lib/api-docs"; +import { NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { - ACMESANType, - CertificateOrderStatus, - CertKeyAlgorithm, - CertSignatureAlgorithm -} from "@app/services/certificate/certificate-types"; +import { CertKeyAlgorithm, CertSignatureAlgorithm } from "@app/services/certificate/certificate-types"; import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; import { CertExtendedKeyUsageType, @@ -21,6 +17,7 @@ import { 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 { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; import { booleanSchema } from "../sanitizedSchemas"; @@ -65,8 +62,10 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { - hide: false, + hide: true, + deprecated: true, tags: [ApiDocsTags.PkiCertificates], + description: "This endpoint will be removed in a future version.", body: z .object({ profileId: z.string().uuid(), @@ -106,7 +105,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => certificateChain: z.string().trim(), privateKey: z.string().trim().optional(), serialNumber: z.string().trim(), - certificateId: z.string() + certificateId: z.string(), + certificateRequestId: z.string() }) } }, @@ -138,6 +138,29 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => removeRootsFromChain: req.body.removeRootsFromChain }); + const certificateRequest = await server.services.certificateRequest.createCertificateRequest({ + status: CertificateRequestStatus.ISSUED, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: data.projectId, + profileId: req.body.profileId, + commonName: req.body.commonName, + altNames: req.body.altNames?.map((altName) => `${altName.type}:${altName.value}`).join(","), + 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, + keyAlgorithm: req.body.keyAlgorithm, + signatureAlgorithm: req.body.signatureAlgorithm + }); + + await server.services.certificateRequest.attachCertificateToRequest({ + certificateRequestId: certificateRequest.id, + certificateId: data.certificateId + }); + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: data.projectId, @@ -152,7 +175,10 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => } }); - return data; + return { + ...data, + certificateRequestId: certificateRequest.id + }; } }); @@ -163,8 +189,10 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { - hide: false, + hide: true, + deprecated: true, tags: [ApiDocsTags.PkiCertificates], + description: "This endpoint will be removed in a future version.", body: z .object({ profileId: z.string().uuid(), @@ -191,14 +219,13 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => issuingCaCertificate: z.string().trim(), certificateChain: z.string().trim(), serialNumber: z.string().trim(), - certificateId: z.string() + certificateId: z.string(), + certificateRequestId: 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, @@ -215,6 +242,32 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => removeRootsFromChain: req.body.removeRootsFromChain }); + const certificateRequestData = extractCertificateRequestFromCSR(req.body.csr); + + const certificateRequest = await server.services.certificateRequest.createCertificateRequest({ + actor: req.permission.type, + status: CertificateRequestStatus.ISSUED, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: data.projectId, + profileId: req.body.profileId, + csr: req.body.csr, + commonName: certificateRequestData.commonName, + altNames: certificateRequestData.subjectAlternativeNames?.map((san) => `${san.type}:${san.value}`).join(","), + keyUsages: certificateRequestData.keyUsages, + extendedKeyUsages: certificateRequestData.extendedKeyUsages, + notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined, + notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined, + keyAlgorithm: certificateRequestData.keyAlgorithm, + signatureAlgorithm: certificateRequestData.signatureAlgorithm + }); + + await server.services.certificateRequest.attachCertificateToRequest({ + certificateRequestId: certificateRequest.id, + certificateId: data.certificateId + }); + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: data.projectId, @@ -224,12 +277,15 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => certificateProfileId: req.body.profileId, certificateId: data.certificateId, profileName: data.profileName, - commonName: certificateRequest.commonName || "" + commonName: certificateRequestData.commonName || "" } } }); - return data; + return { + ...data, + certificateRequestId: certificateRequest.id + }; } }); @@ -240,23 +296,23 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => rateLimit: writeLimit }, schema: { - hide: false, + hide: true, + deprecated: true, tags: [ApiDocsTags.PkiCertificates], + description: "This endpoint will be removed in a future version.", 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"), + subjectAlternativeNames: z.array( + z.object({ + type: z.nativeEnum(CertSubjectAlternativeNameType), + value: z + .string() + .trim() + .min(1, "SAN value cannot be empty") + .max(255, "SAN value must be less than 255 characters") + }) + ), ttl: z .string() .trim() @@ -280,62 +336,55 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => }), 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() + certificate: z.string().optional(), + certificateRequestId: z.string() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { + const certificateOrderObject = { + 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 + }; + 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 - }, + certificateOrder: certificateOrderObject, removeRootsFromChain: req.body.removeRootsFromChain }); + const certificateRequest = await server.services.certificateRequest.createCertificateRequest({ + status: CertificateRequestStatus.PENDING, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: data.projectId, + profileId: req.body.profileId, + commonName: req.body.commonName, + altNames: req.body.subjectAlternativeNames?.map((san) => `${san.type}:${san.value}`).join(","), + 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 + }); + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: data.projectId, @@ -343,13 +392,15 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, metadata: { certificateProfileId: req.body.profileId, - orderId: data.orderId, profileName: data.profileName } } }); - return data; + return { + ...data, + certificateRequestId: certificateRequest.id + }; } }); @@ -377,12 +428,24 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => certificateChain: z.string().trim(), privateKey: z.string().trim().optional(), serialNumber: z.string().trim(), - certificateId: z.string() + certificateId: z.string(), + certificateRequestId: z.string() }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { + const originalCertificate = await server.services.certificate.getCert({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.certificateId + }); + if (!originalCertificate) { + throw new NotFoundError({ message: "Original certificate not found" }); + } + const data = await server.services.certificateV3.renewCertificate({ actor: req.permission.type, actorId: req.permission.id, 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 5f09d61a8..e7e2bca76 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 a79b235cd..f28508efb 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"; @@ -172,7 +177,8 @@ const PKI_APP_CONNECTIONS = [ AppConnection.Cloudflare, AppConnection.AzureADCS, AppConnection.AzureKeyVault, - AppConnection.Chef + AppConnection.Chef, + AppConnection.DNSMadeEasy ]; export const listAppConnectionOptions = (projectType?: ProjectType) => { @@ -208,6 +214,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => { getFlyioConnectionListItem(), getGitLabConnectionListItem(), getCloudflareConnectionListItem(), + getDNSMadeEasyConnectionListItem(), getZabbixConnectionListItem(), getRailwayConnectionListItem(), getBitbucketConnectionListItem(), @@ -341,6 +348,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, @@ -398,6 +406,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: @@ -487,6 +497,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 49b337a2c..a41589d12 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", @@ -78,6 +79,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/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index e2f0f5f16..8e0f654a3 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -663,7 +663,8 @@ export const authLoginServiceFactory = ({ timestamp: new Date().toISOString(), ip: ipAddress, userAgent, - siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com") + siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com"), + orgId: organizationId }, template: SmtpTemplates.OrgAdminBreakglassAccess }); 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 ff95083c6..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 @@ -1,6 +1,7 @@ import * as x509 from "@peculiar/x509"; import acme, { CsrBuffer } from "acme-client"; import { Knex } from "knex"; +import RE2 from "re2"; import { TableName } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; @@ -14,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"; @@ -23,6 +25,7 @@ import { CertKeyUsage, CertStatus } from "@app/services/certificate/certificate-types"; +import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal"; @@ -43,8 +46,63 @@ 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 => { + const match = ttl.match(new RE2("^(\\d+)([dhm])$")); + if (!match) { + throw new BadRequestError({ message: `Invalid TTL format: ${ttl}` }); + } + + const [, value, unit] = match; + const num = parseInt(value, 10); + + switch (unit) { + case "d": + return num; + case "h": + return Math.ceil(num / 24); + case "m": + return Math.ceil(num / (24 * 60)); + default: + throw new BadRequestError({ message: `Invalid TTL unit: ${unit}` }); + } +}; + +const calculateRenewalThreshold = ( + profileRenewBeforeDays: number | undefined, + certificateTtlInDays: number +): number | undefined => { + if (profileRenewBeforeDays === undefined) { + return undefined; + } + + if (profileRenewBeforeDays >= certificateTtlInDays) { + return Math.max(1, certificateTtlInDays - 1); + } + + return profileRenewBeforeDays; +}; + +const calculateFinalRenewBeforeDays = ( + profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } } | undefined, + ttl: string +): number | undefined => { + if (!profile?.apiConfig?.autoRenew || !profile.apiConfig.renewBeforeDays) { + return undefined; + } + + const certificateTtlInDays = parseTtlToDays(ttl); + const renewBeforeDays = calculateRenewalThreshold(profile.apiConfig.renewBeforeDays, certificateTtlInDays); + + if (!renewBeforeDays) { + return undefined; + } + + return renewBeforeDays; +}; + type TAcmeCertificateAuthorityFnsDeps = { appConnectionDAL: Pick; appConnectionService: Pick; @@ -53,7 +111,7 @@ type TAcmeCertificateAuthorityFnsDeps = { "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById" >; externalCertificateAuthorityDAL: Pick; - certificateDAL: Pick; + certificateDAL: Pick; certificateBodyDAL: Pick; certificateSecretDAL: Pick; kmsService: Pick< @@ -64,13 +122,14 @@ type TAcmeCertificateAuthorityFnsDeps = { pkiSyncDAL: Pick; pkiSyncQueue: Pick; projectDAL: Pick; + certificateProfileDAL?: Pick; }; type TOrderCertificateDeps = { appConnectionDAL: Pick; certificateAuthorityDAL: Pick; externalCertificateAuthorityDAL: Pick; - certificateDAL: Pick; + certificateDAL: Pick; certificateBodyDAL: Pick; certificateSecretDAL: Pick; kmsService: Pick< @@ -78,6 +137,7 @@ type TOrderCertificateDeps = { "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" >; projectDAL: Pick; + certificateProfileDAL?: Pick; }; type DBConfigurationColumn = { @@ -91,7 +151,7 @@ type DBConfigurationColumn = { export const castDbEntryToAcmeCertificateAuthority = ( ca: Awaited> -): TAcmeCertificateAuthority & { credentials: unknown } => { +): TAcmeCertificateAuthority & { credentials: Buffer | null | undefined } => { if (!ca.externalCa?.id) { throw new BadRequestError({ message: "Malformed ACME certificate authority" }); } @@ -120,18 +180,41 @@ 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, + profileId, subscriberId, commonName, altNames, csr, csrPrivateKey, keyUsages, - extendedKeyUsages + extendedKeyUsages, + ttl, + signatureAlgorithm, + keyAlgorithm, + isRenewal, + originalCertificateId }: { caId: string; + profileId?: string; subscriberId?: string; commonName: string; altNames?: string[]; @@ -139,6 +222,11 @@ export const orderCertificate = async ( csrPrivateKey?: string; keyUsages?: CertKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[]; + ttl?: string; + signatureAlgorithm?: string; + keyAlgorithm?: string; + isRenewal?: boolean; + originalCertificateId?: string; }, deps: TOrderCertificateDeps, tx?: Knex @@ -151,7 +239,8 @@ export const orderCertificate = async ( certificateBodyDAL, certificateSecretDAL, kmsService, - projectDAL + projectDAL, + certificateProfileDAL } = deps; const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx); @@ -181,7 +270,7 @@ export const orderCertificate = async ( let accountKey: Buffer | undefined; if (acmeCa.credentials) { const decryptedCredentials = await kmsDecryptor({ - cipherTextBlob: acmeCa.credentials as Buffer + cipherTextBlob: acmeCa.credentials }); const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync( @@ -241,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: { @@ -263,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: { @@ -291,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}`); } @@ -322,6 +435,7 @@ export const orderCertificate = async ( { caId: ca.id, pkiSubscriberId: subscriberId, + profileId, status: CertStatus.ACTIVE, friendlyName: commonName, commonName, @@ -331,11 +445,18 @@ export const orderCertificate = async ( notAfter: certObj.notAfter, keyUsages, extendedKeyUsages, - projectId: ca.projectId + keyAlgorithm, + signatureAlgorithm, + projectId: ca.projectId, + renewedFromCertificateId: isRenewal && originalCertificateId ? originalCertificateId : null }, innerTx ); + if (isRenewal && originalCertificateId) { + await certificateDAL.updateById(originalCertificateId, { renewedByCertificateId: cert.id }, innerTx); + } + await certificateBodyDAL.create( { certId: cert.id, @@ -355,6 +476,26 @@ export const orderCertificate = async ( ); } + if (profileId && ttl && certificateProfileDAL) { + const profile = await certificateProfileDAL.findById(profileId, innerTx); + if (profile) { + const finalRenewBeforeDays = calculateFinalRenewBeforeDays( + profile as { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } }, + ttl + ); + + if (finalRenewBeforeDays !== undefined) { + await certificateDAL.updateById( + cert.id, + { + renewBeforeDays: finalRenewBeforeDays + }, + innerTx + ); + } + } + } + return cert; }); }; @@ -371,13 +512,13 @@ export const AcmeCertificateAuthorityFns = ({ projectDAL, pkiSubscriberDAL, pkiSyncDAL, - pkiSyncQueue + pkiSyncQueue, + certificateProfileDAL }: TAcmeCertificateAuthorityFnsDeps) => { const createCertificateAuthority = async ({ name, projectId, configuration, - enableDirectIssuance, actor, status }: { @@ -385,7 +526,6 @@ export const AcmeCertificateAuthorityFns = ({ name: string; projectId: string; configuration: TCreateAcmeCertificateAuthorityDTO["configuration"]; - enableDirectIssuance: boolean; actor: OrgServiceActor; }) => { if (crypto.isFipsModeEnabled()) { @@ -413,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, @@ -425,7 +571,7 @@ export const AcmeCertificateAuthorityFns = ({ const ca = await certificateAuthorityDAL.create( { projectId, - enableDirectIssuance, + enableDirectIssuance: false, name, status }, @@ -473,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; }) => { @@ -508,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) { @@ -541,13 +694,12 @@ export const AcmeCertificateAuthorityFns = ({ ); } - if (name || status || enableDirectIssuance) { + if (name || status) { await certificateAuthorityDAL.updateById( id, { name, - status, - enableDirectIssuance + status }, tx ); @@ -616,10 +768,71 @@ export const AcmeCertificateAuthorityFns = ({ await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue }); }; + const orderCertificateFromProfile = async ({ + caId, + profileId, + commonName, + altNames = [], + csr, + csrPrivateKey, + keyUsages, + extendedKeyUsages, + ttl, + signatureAlgorithm, + keyAlgorithm, + isRenewal, + originalCertificateId + }: { + caId: string; + profileId?: string; + commonName: string; + altNames?: string[]; + csr: CsrBuffer; + csrPrivateKey: string; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; + ttl?: string; + signatureAlgorithm?: string; + keyAlgorithm?: string; + isRenewal?: boolean; + originalCertificateId?: string; + }) => { + return orderCertificate( + { + caId, + profileId, + subscriberId: undefined, + commonName, + altNames, + csr, + csrPrivateKey, + keyUsages, + extendedKeyUsages, + ttl, + signatureAlgorithm, + keyAlgorithm, + isRenewal, + originalCertificateId + }, + { + appConnectionDAL, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + projectDAL, + certificateProfileDAL + } + ); + }; + return { createCertificateAuthority, updateCertificateAuthority, listCertificateAuthorities, - orderSubscriberCertificate + orderSubscriberCertificate, + orderCertificateFromProfile }; }; 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 26f59a402..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 @@ -21,8 +21,10 @@ import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage, - CertStatus + CertStatus, + TAltNameType } from "@app/services/certificate/certificate-types"; +import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { TPkiSubscriberProperties } from "@app/services/pki-subscriber/pki-subscriber-types"; @@ -42,6 +44,60 @@ import { TUpdateAzureAdCsCertificateAuthorityDTO } from "./azure-ad-cs-certificate-authority-types"; +const parseTtlToDays = (ttl: string): number => { + const match = ttl.match(new RE2("^(\\d+)([dhm])$")); + if (!match) { + throw new BadRequestError({ message: `Invalid TTL format: ${ttl}` }); + } + + const [, value, unit] = match; + const num = parseInt(value, 10); + + switch (unit) { + case "d": + return num; + case "h": + return Math.ceil(num / 24); + case "m": + return Math.ceil(num / (24 * 60)); + default: + throw new BadRequestError({ message: `Invalid TTL unit: ${unit}` }); + } +}; + +const calculateRenewalThreshold = ( + profileRenewBeforeDays: number | undefined, + certificateTtlInDays: number +): number | undefined => { + if (profileRenewBeforeDays === undefined) { + return undefined; + } + + if (profileRenewBeforeDays >= certificateTtlInDays) { + return Math.max(1, certificateTtlInDays - 1); + } + + return profileRenewBeforeDays; +}; + +const calculateFinalRenewBeforeDays = ( + profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } } | undefined, + ttl: string +): number | undefined => { + const hasAutoRenewEnabled = profile?.apiConfig?.autoRenew === true; + if (!hasAutoRenewEnabled) { + return undefined; + } + + const profileRenewBeforeDays = profile?.apiConfig?.renewBeforeDays; + if (profileRenewBeforeDays !== undefined) { + const certificateTtlInDays = parseTtlToDays(ttl); + return calculateRenewalThreshold(profileRenewBeforeDays, certificateTtlInDays); + } + + return undefined; +}; + type TAzureAdCsCertificateAuthorityFnsDeps = { appConnectionDAL: Pick; appConnectionService: Pick; @@ -50,7 +106,7 @@ type TAzureAdCsCertificateAuthorityFnsDeps = { "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById" >; externalCertificateAuthorityDAL: Pick; - certificateDAL: Pick; + certificateDAL: Pick; certificateBodyDAL: Pick; certificateSecretDAL: Pick; kmsService: Pick< @@ -61,6 +117,7 @@ type TAzureAdCsCertificateAuthorityFnsDeps = { pkiSyncDAL: Pick; pkiSyncQueue: Pick; projectDAL: Pick; + certificateProfileDAL?: Pick; }; type AzureCertificateRequest = { @@ -190,7 +247,7 @@ const buildSubjectDN = (commonName: string, properties?: TPkiSubscriberPropertie export const castDbEntryToAzureAdCsCertificateAuthority = ( ca: Awaited> -): TAzureAdCsCertificateAuthority & { credentials: unknown } => { +): TAzureAdCsCertificateAuthority & { credentials: Buffer | null | undefined } => { if (!ca.externalCa?.id) { throw new BadRequestError({ message: "Malformed Active Directory Certificate Service certificate authority" }); } @@ -591,13 +648,13 @@ export const AzureAdCsCertificateAuthorityFns = ({ projectDAL, pkiSubscriberDAL, pkiSyncDAL, - pkiSyncQueue + pkiSyncQueue, + certificateProfileDAL }: TAzureAdCsCertificateAuthorityFnsDeps) => { const createCertificateAuthority = async ({ name, projectId, configuration, - enableDirectIssuance, actor, status }: { @@ -605,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); @@ -679,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; @@ -737,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 ); @@ -1043,6 +1082,384 @@ export const AzureAdCsCertificateAuthorityFns = ({ }; }; + const orderCertificateFromProfile = async ({ + caId, + profileId, + commonName, + altNames = [], + keyUsages = [], + extendedKeyUsages = [], + template, + validity, + notBefore, + notAfter, + signatureAlgorithm, + keyAlgorithm = CertKeyAlgorithm.RSA_2048, + isRenewal, + originalCertificateId + }: { + caId: string; + profileId: string; + commonName: string; + altNames?: string[]; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; + template?: string; + validity: { ttl: string }; + notBefore?: Date; + notAfter?: Date; + signatureAlgorithm?: string; + keyAlgorithm?: CertKeyAlgorithm; + isRenewal?: boolean; + originalCertificateId?: string; + }) => { + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca.externalCa || ca.externalCa.type !== CaType.AZURE_AD_CS) { + throw new BadRequestError({ message: "CA is not an Active Directory Certificate Service CA" }); + } + + const azureCa = castDbEntryToAzureAdCsCertificateAuthority(ca); + if (azureCa.status !== CaStatus.ACTIVE) { + throw new BadRequestError({ message: "CA is disabled" }); + } + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + const { username, password, adcsUrl, sslRejectUnauthorized, sslCertificate } = + await getAzureADCSConnectionCredentials( + azureCa.configuration.azureAdcsConnectionId, + appConnectionDAL, + kmsService + ); + + const credentials: { + username: string; + password: string; + sslRejectUnauthorized?: boolean; + sslCertificate?: string; + } = { + username, + password, + sslRejectUnauthorized, + sslCertificate + }; + + let alg; + if (signatureAlgorithm) { + switch (signatureAlgorithm.toUpperCase()) { + case "RSA-SHA256": + case "SHA256WITHRSA": + alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + break; + case "RSA-SHA384": + case "SHA384WITHRSA": + alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_3072); + break; + case "RSA-SHA512": + case "SHA512WITHRSA": + alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_4096); + break; + case "ECDSA-SHA256": + case "SHA256WITHECDSA": + alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.ECDSA_P256); + break; + case "ECDSA-SHA384": + case "SHA384WITHECDSA": + alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.ECDSA_P384); + break; + case "ECDSA-SHA512": + case "SHA512WITHECDSA": + alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.ECDSA_P521); + break; + default: + alg = keyAlgorithmToAlgCfg(keyAlgorithm); + break; + } + } else { + alg = keyAlgorithmToAlgCfg(keyAlgorithm); + } + + const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey); + const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; + + const subjectDN = buildSubjectDN(commonName); + + let sanExtension = ""; + if (altNames && altNames.length > 0) { + sanExtension = altNames.join(","); + } + + const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ + name: subjectDN, + keys: leafKeys, + signingAlgorithm: alg, + ...(sanExtension && { + extensions: [ + new x509.SubjectAlternativeNameExtension( + altNames.map((name) => ({ type: "dns" as TAltNameType, value: name })), + false + ) + ] + }) + }); + + const csrPem = csrObj.toString("pem"); + + let templateValue = template; + if (!templateValue) { + templateValue = "WebServer"; + } + + const templateInput = templateValue.trim(); + if (!templateInput || templateInput.length === 0) { + throw new BadRequestError({ + message: "Certificate template name cannot be empty" + }); + } + + let validityPeriod: string | undefined; + if (notBefore && notAfter) { + if (notAfter <= notBefore) { + throw new BadRequestError({ + message: "Certificate notAfter date must be after notBefore date" + }); + } + + const diffMs = notAfter.getTime() - notBefore.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + validityPeriod = `${diffDays}d`; + } else if (notAfter) { + const diffMs = notAfter.getTime() - Date.now(); + if (diffMs <= 0) { + throw new BadRequestError({ + message: "Certificate notAfter date must be in the future" + }); + } + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + validityPeriod = `${diffDays}d`; + } else if (validity.ttl) { + validityPeriod = validity.ttl; + } + + const certificateRequest: AzureCertificateRequest = { + csr: csrPem, + template: templateInput, + attributes: { + subject: subjectDN, + ...(sanExtension && { subjectAlternativeName: sanExtension }), + ...(validityPeriod && { validityPeriod }) + } + }; + + let submissionResponse; + const maxOidRetries = 3; + let oidRetryCount = 0; + + while (oidRetryCount <= maxOidRetries) { + try { + submissionResponse = await submitCertificateRequest(credentials, adcsUrl, certificateRequest); + break; + } catch (error) { + const isOidError = + error instanceof BadRequestError && + (error.message.includes("OID resolution error") || error.message.includes("Cannot get OID for name type")); + + if (isOidError && oidRetryCount < maxOidRetries) { + oidRetryCount += 1; + + const delay = 3000 * oidRetryCount; + await new Promise((resolve) => { + setTimeout(resolve, delay); + }); + // eslint-disable-next-line no-continue + continue; + } + + throw error; + } + } + + if (!submissionResponse) { + throw new BadRequestError({ + message: "Failed to submit certificate request after multiple attempts due to OID resolution issues" + }); + } + + if (submissionResponse.status === "denied") { + throw new BadRequestError({ message: "Certificate request was denied by ADCS" }); + } + + let certificatePem = ""; + + if (submissionResponse.status === "issued" && submissionResponse.certificate) { + certificatePem = submissionResponse.certificate; + } else { + const maxRetries = 5; + const initialDelay = 2000; + let retryCount = 0; + let lastError: Error | null = null; + + // eslint-disable-next-line no-await-in-loop + while (retryCount < maxRetries) { + try { + // eslint-disable-next-line no-await-in-loop + certificatePem = await retrieveCertificate(credentials, adcsUrl, submissionResponse.certificateId); + break; + } catch (error) { + lastError = error as Error; + // eslint-disable-next-line no-plusplus + retryCount++; + + if (retryCount < maxRetries) { + // Wait with exponential backoff: 2s, 4s, 8s, 16s, 32s + const delay = initialDelay * 2 ** (retryCount - 1); + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, delay); + }); + } + } + } + + if (retryCount === maxRetries) { + throw new BadRequestError({ + message: `Certificate request submitted with ID ${submissionResponse.certificateId} but failed to retrieve after ${maxRetries} attempts. The certificate may still be pending approval or processing. Last error: ${lastError?.message || "Unknown error"}.` + }); + } + } + + if (!certificatePem) { + throw new BadRequestError({ + message: "Failed to obtain certificate from ADCS. The certificate may still be pending processing." + }); + } + + let cleanedCertificatePem = certificatePem.trim(); + + if (!cleanedCertificatePem.includes("-----BEGIN CERTIFICATE-----")) { + throw new BadRequestError({ + message: "Invalid certificate format received from ADCS. Expected PEM format." + }); + } + + cleanedCertificatePem = cleanedCertificatePem + .replace(new RE2("\\r\\n", "g"), "\n") + .replace(new RE2("\\r", "g"), "\n") + .trim(); + + if (!cleanedCertificatePem.includes("-----END CERTIFICATE-----")) { + throw new BadRequestError({ + message: "Invalid certificate format received from ADCS. Missing end marker." + }); + } + + let certObj: x509.X509Certificate; + try { + certObj = new x509.X509Certificate(cleanedCertificatePem); + } catch (error) { + throw new BadRequestError({ + message: `Failed to parse certificate from ADCS: ${error instanceof Error ? error.message : "Unknown error"}. Certificate data may be corrupted.` + }); + } + + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(certObj.rawData)) + }); + + const certificateChainPem = submissionResponse.certificateChain || ""; + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); + + const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ + plainText: Buffer.from(skLeaf) + }); + + let certificateId: string; + + await certificateDAL.transaction(async (tx) => { + const cert = await certificateDAL.create( + { + caId: ca.id, + profileId, + status: CertStatus.ACTIVE, + friendlyName: commonName, + commonName, + altNames: altNames.join(","), + serialNumber: certObj.serialNumber, + notBefore: certObj.notBefore, + notAfter: certObj.notAfter, + keyUsages, + extendedKeyUsages, + keyAlgorithm, + signatureAlgorithm, + projectId: ca.projectId, + renewedFromCertificateId: isRenewal && originalCertificateId ? originalCertificateId : null + }, + tx + ); + + certificateId = cert.id; + + if (isRenewal && originalCertificateId) { + await certificateDAL.updateById(originalCertificateId, { renewedByCertificateId: cert.id }, tx); + } + + await certificateBodyDAL.create( + { + certId: cert.id, + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + await certificateSecretDAL.create( + { + certId: cert.id, + encryptedPrivateKey + }, + tx + ); + + if (profileId && validity?.ttl && certificateProfileDAL) { + const profile = await certificateProfileDAL.findById(profileId, tx); + if (profile) { + const finalRenewBeforeDays = calculateFinalRenewBeforeDays(undefined, validity.ttl); + + if (finalRenewBeforeDays !== undefined) { + await certificateDAL.updateById( + cert.id, + { + renewBeforeDays: finalRenewBeforeDays + }, + tx + ); + } + } + } + }); + + return { + certificate: cleanedCertificatePem, + certificateChain: certificateChainPem, + privateKey: skLeaf, + serialNumber: certObj.serialNumber, + certificateId: certificateId!, + ca: azureCa + }; + }; + const getTemplates = async ({ caId, projectId }: { caId: string; projectId: string }) => { const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); if (!ca || ca.projectId !== projectId) { @@ -1182,6 +1599,7 @@ export const AzureAdCsCertificateAuthorityFns = ({ updateCertificateAuthority, listCertificateAuthorities, orderSubscriberCertificate, + orderCertificateFromProfile, getTemplates }; }; diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas.ts index 2c2dfe484..004e3d4a5 100644 --- a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas.ts +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas.ts @@ -11,6 +11,13 @@ export const AzureAdCsCertificateAuthorityConfigurationSchema = z.object({ azureAdcsConnectionId: z.string().uuid().trim().describe("Azure ADCS Connection ID") }); +export const AzureAdCsCertificateAuthorityCredentialsSchema = z.object({ + username: z.string(), + password: z.string(), + sslRejectUnauthorized: z.boolean().optional(), + sslCertificate: z.string().optional() +}); + export const AzureAdCsCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({ type: z.literal(CaType.AZURE_AD_CS), configuration: AzureAdCsCertificateAuthorityConfigurationSchema 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 ed27f7571..be53f08ce 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -11,6 +11,7 @@ import { TAppConnectionServiceFactory } from "../app-connection/app-connection-s import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; +import { TCertificateProfileDALFactory } from "../certificate-profile/certificate-profile-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal"; @@ -37,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"; @@ -62,7 +64,7 @@ type TCertificateAuthorityServiceFactoryDep = { internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory; projectDAL: Pick; permissionService: Pick; - certificateDAL: Pick; + certificateDAL: Pick; certificateBodyDAL: Pick; certificateSecretDAL: Pick; kmsService: Pick< @@ -72,6 +74,7 @@ type TCertificateAuthorityServiceFactoryDep = { pkiSubscriberDAL: Pick; pkiSyncDAL: Pick; pkiSyncQueue: Pick; + certificateProfileDAL?: Pick; }; export type TCertificateAuthorityServiceFactory = ReturnType; @@ -90,7 +93,8 @@ export const certificateAuthorityServiceFactory = ({ kmsService, pkiSubscriberDAL, pkiSyncDAL, - pkiSyncQueue + pkiSyncQueue, + certificateProfileDAL }: TCertificateAuthorityServiceFactoryDep) => { const acmeFns = AcmeCertificateAuthorityFns({ appConnectionDAL, @@ -104,7 +108,8 @@ export const certificateAuthorityServiceFactory = ({ pkiSubscriberDAL, projectDAL, pkiSyncDAL, - pkiSyncQueue + pkiSyncQueue, + certificateProfileDAL }); const azureAdCsFns = AzureAdCsCertificateAuthorityFns({ @@ -119,11 +124,12 @@ export const certificateAuthorityServiceFactory = ({ pkiSubscriberDAL, projectDAL, pkiSyncDAL, - pkiSyncQueue + pkiSyncQueue, + certificateProfileDAL }); const createCertificateAuthority = async ( - { type, projectId, name, enableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO, + { type, projectId, name, configuration, status }: TCreateCertificateAuthorityDTO, actor: OrgServiceActor ) => { const { permission } = await permissionService.getProjectPermission({ @@ -145,7 +151,6 @@ export const certificateAuthorityServiceFactory = ({ ...(configuration as TCreateInternalCertificateAuthorityDTO["configuration"]), isInternal: true, projectId, - enableDirectIssuance, name }); @@ -171,7 +176,6 @@ export const certificateAuthorityServiceFactory = ({ name, projectId, configuration: configuration as TCreateAcmeCertificateAuthorityDTO["configuration"], - enableDirectIssuance, status, actor }); @@ -182,7 +186,6 @@ export const certificateAuthorityServiceFactory = ({ name, projectId, configuration: configuration as TCreateAzureAdCsCertificateAuthorityDTO["configuration"], - enableDirectIssuance, status, actor }); @@ -191,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 @@ -303,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( @@ -339,7 +537,6 @@ export const certificateAuthorityServiceFactory = ({ const updatedCa = await internalCertificateAuthorityService.updateCaById({ isInternal: true, - enableDirectIssuance, caId: certificateAuthority.id, status, name @@ -366,7 +563,6 @@ export const certificateAuthorityServiceFactory = ({ return acmeFns.updateCertificateAuthority({ id: certificateAuthority.id, configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"], - enableDirectIssuance, actor, status, name @@ -377,7 +573,6 @@ export const certificateAuthorityServiceFactory = ({ return azureAdCsFns.updateCertificateAuthority({ id: certificateAuthority.id, configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"], - enableDirectIssuance, actor, status, name @@ -387,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 ) => { @@ -487,12 +682,51 @@ export const certificateAuthorityServiceFactory = ({ }); }; + const getCaById = async ({ + caId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: { + caId: string; + actor: OrgServiceActor["type"]; + actorId: string; + actorAuthMethod: OrgServiceActor["authMethod"]; + actorOrgId?: string; + }) => { + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca) { + throw new NotFoundError({ message: "CA not found" }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: ca.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateAuthorities + ); + + return ca; + }; + return { createCertificateAuthority, - findCertificateAuthorityByNameAndProjectId, + findCertificateAuthorityById, listCertificateAuthoritiesByProjectId, + findCertificateAuthorityByNameAndProjectId, updateCertificateAuthority, deleteCertificateAuthority, - getAzureAdcsTemplates + getAzureAdcsTemplates, + 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/certificate-issuance-queue.ts b/backend/src/services/certificate-authority/certificate-issuance-queue.ts new file mode 100644 index 000000000..f590f2850 --- /dev/null +++ b/backend/src/services/certificate-authority/certificate-issuance-queue.ts @@ -0,0 +1,377 @@ +import acme from "acme-client"; + +import { crypto } from "@app/lib/crypto/cryptography"; +import { NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, TQueueServiceFactory } from "@app/queue"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; +import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service"; +import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; +import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; +import { CertKeyAlgorithm } from "../certificate-common/certificate-constants"; +import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service"; +import { CertificateRequestStatus } from "../certificate-request/certificate-request-types"; +import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; +import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal"; +import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue"; +import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns"; +import { AzureAdCsCertificateAuthorityFns } from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns"; +import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; +import { CaType } from "./certificate-authority-enums"; +import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; +import { TExternalCertificateAuthorityDALFactory } from "./external-certificate-authority-dal"; + +export type TIssueCertificateFromProfileJobData = { + certificateId: string; + profileId: string; + caId: string; + commonName?: string; + altNames?: string[]; + ttl: string; + signatureAlgorithm: string; + keyAlgorithm: string; + keyUsages?: string[]; + extendedKeyUsages?: string[]; + isRenewal?: boolean; + originalCertificateId?: string; + certificateRequestId?: string; + csr?: string; +}; + +type TCertificateIssuanceQueueFactoryDep = { + certificateAuthorityDAL: TCertificateAuthorityDALFactory; + appConnectionDAL: Pick; + appConnectionService: Pick; + externalCertificateAuthorityDAL: Pick; + certificateDAL: TCertificateDALFactory; + projectDAL: Pick; + kmsService: Pick< + TKmsServiceFactory, + "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "createCipherPairWithDataKey" + >; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + queueService: TQueueServiceFactory; + pkiSubscriberDAL: Pick; + pkiSyncDAL: Pick; + pkiSyncQueue: Pick; + certificateProfileDAL?: Pick; + certificateRequestService?: Pick< + TCertificateRequestServiceFactory, + "attachCertificateToRequest" | "updateCertificateRequestStatus" + >; +}; + +export type TCertificateIssuanceQueueFactory = ReturnType; + +export const certificateIssuanceQueueFactory = ({ + certificateAuthorityDAL, + appConnectionDAL, + appConnectionService, + externalCertificateAuthorityDAL, + certificateDAL, + projectDAL, + kmsService, + queueService, + certificateBodyDAL, + certificateSecretDAL, + pkiSubscriberDAL, + pkiSyncDAL, + pkiSyncQueue, + certificateProfileDAL, + certificateRequestService +}: TCertificateIssuanceQueueFactoryDep) => { + const acmeFns = AcmeCertificateAuthorityFns({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + pkiSubscriberDAL, + projectDAL, + pkiSyncDAL, + pkiSyncQueue, + certificateProfileDAL + }); + + const azureAdCsFns = AzureAdCsCertificateAuthorityFns({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + pkiSubscriberDAL, + projectDAL, + pkiSyncDAL, + pkiSyncQueue, + certificateProfileDAL + }); + + /** + * Queue a certificate issuance job using pgBoss + */ + const queueCertificateIssuance = async ({ + certificateId, + profileId, + caId, + commonName, + altNames, + ttl, + signatureAlgorithm, + keyAlgorithm, + keyUsages, + extendedKeyUsages, + isRenewal, + originalCertificateId, + certificateRequestId, + csr + }: TIssueCertificateFromProfileJobData) => { + const jobData: TIssueCertificateFromProfileJobData = { + certificateId, + profileId, + caId, + commonName, + altNames, + ttl, + signatureAlgorithm, + keyAlgorithm, + keyUsages, + extendedKeyUsages, + isRenewal, + originalCertificateId, + certificateRequestId, + csr + }; + + await queueService.queuePg(QueueJobs.CaIssueCertificateFromProfile, jobData, { + retryLimit: 3, + retryDelay: 5, + retryBackoff: true + }); + }; + + /** + * Process certificate issuance jobs + */ + const processCertificateIssuanceJobs = async (data: TIssueCertificateFromProfileJobData) => { + const { + certificateId, + profileId, + caId, + commonName, + altNames, + ttl, + signatureAlgorithm, + keyAlgorithm, + keyUsages, + extendedKeyUsages, + isRenewal, + originalCertificateId, + certificateRequestId, + csr + } = data; + + try { + logger.info(`Processing certificate issuance job for [certificateId=${certificateId}] [caId=${caId}]`); + + if (!caId) { + throw new NotFoundError({ + message: `Certificate authority ID is required for external CA certificate issuance` + }); + } + + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + + if (ca.externalCa?.type === CaType.ACME) { + let certificateCsr: string; + let skLeaf: string = ""; + + if (csr) { + certificateCsr = csr; + } else { + const keyAlg = keyAlgorithmToAlgCfg(keyAlgorithm as CertKeyAlgorithm); + const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyAlg, true, ["sign", "verify"]); + const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey); + skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; + + const [, generatedCsr] = await acme.crypto.createCsr( + { + altNames: altNames ? [...altNames] : [], + commonName: commonName || "" + }, + skLeaf + ); + certificateCsr = generatedCsr.toString(); + } + + const acmeResult = await acmeFns.orderCertificateFromProfile({ + caId, + profileId, + commonName: commonName || "", + altNames: altNames || [], + csr: Buffer.from(certificateCsr), + csrPrivateKey: skLeaf, + keyUsages: keyUsages as CertKeyUsage[], + extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[], + ttl, + signatureAlgorithm, + keyAlgorithm, + isRenewal, + originalCertificateId + }); + + if (certificateRequestId && certificateRequestService && acmeResult?.id) { + try { + await certificateRequestService.attachCertificateToRequest({ + certificateRequestId, + certificateId: acmeResult.id + }); + logger.info(`Certificate attached to request [certificateRequestId=${certificateRequestId}]`); + } catch (attachError) { + logger.error( + attachError, + `Failed to attach certificate to request [certificateRequestId=${certificateRequestId}]` + ); + try { + await certificateRequestService.updateCertificateRequestStatus({ + certificateRequestId, + status: CertificateRequestStatus.FAILED, + errorMessage: `Failed to attach certificate: ${attachError instanceof Error ? attachError.message : String(attachError)}` + }); + } catch (statusUpdateError) { + logger.error( + statusUpdateError, + `Failed to update certificate request status [certificateRequestId=${certificateRequestId}]` + ); + } + } + } + } else if (ca.externalCa?.type === CaType.AZURE_AD_CS) { + let template: string | undefined; + if (certificateProfileDAL) { + try { + const profile = await certificateProfileDAL.findById(profileId); + if ( + profile?.externalConfigs && + typeof profile.externalConfigs === "object" && + profile.externalConfigs !== null + ) { + const configs = profile.externalConfigs; + if (typeof configs.template === "string") { + template = configs.template; + } + } + } catch (error) { + logger.warn( + `Failed to fetch profile ${profileId} for template extraction: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + const azureParams = { + caId, + profileId, + commonName: commonName || "", + altNames: altNames || [], + keyUsages: keyUsages as CertKeyUsage[], + extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[], + validity: { ttl }, + signatureAlgorithm, + keyAlgorithm: keyAlgorithm as CertKeyAlgorithm, + isRenewal, + originalCertificateId, + template, + ...(csr && { csr }) + }; + + const azureResult = await azureAdCsFns.orderCertificateFromProfile(azureParams); + + if (certificateRequestId && certificateRequestService && azureResult?.certificateId) { + try { + await certificateRequestService.attachCertificateToRequest({ + certificateRequestId, + certificateId: azureResult.certificateId + }); + logger.info(`Certificate attached to request [certificateRequestId=${certificateRequestId}]`); + } catch (attachError) { + logger.error( + attachError, + `Failed to attach certificate to request [certificateRequestId=${certificateRequestId}]` + ); + try { + await certificateRequestService.updateCertificateRequestStatus({ + certificateRequestId, + status: CertificateRequestStatus.FAILED, + errorMessage: `Failed to attach certificate: ${attachError instanceof Error ? attachError.message : String(attachError)}` + }); + } catch (statusUpdateError) { + logger.error( + statusUpdateError, + `Failed to update certificate request status [certificateRequestId=${certificateRequestId}]` + ); + } + } + } + } + + logger.info( + `Successfully processed certificate issuance job with [certificateId=${certificateId}] [caId=${caId}]` + ); + } catch (error: unknown) { + logger.error(error, `Certificate issuance job failed for [certificateId=${certificateId}] [caId=${caId}]`); + + if (certificateRequestId && certificateRequestService) { + try { + await certificateRequestService.updateCertificateRequestStatus({ + certificateRequestId, + status: CertificateRequestStatus.FAILED, + errorMessage: `Certificate issuance failed: ${error instanceof Error ? error.message : String(error)}` + }); + logger.info(`Updated certificate request ${certificateRequestId} status to failed due to issuance error`); + } catch (statusUpdateError) { + logger.error( + statusUpdateError, + `Failed to update certificate request status [certificateRequestId=${certificateRequestId}]` + ); + } + } + + throw error; + } + }; + + const initializeCertificateIssuanceQueue = async () => { + await queueService.startPg( + QueueJobs.CaIssueCertificateFromProfile, + async ([job]) => { + const data = job.data as TIssueCertificateFromProfileJobData; + await processCertificateIssuanceJobs(data); + }, + { + workerCount: 2, + batchSize: 1, + pollingIntervalSeconds: 1 + } + ); + + logger.info("Certificate issuance queue worker initialized successfully"); + }; + + return { + queueCertificateIssuance, + initializeCertificateIssuanceQueue, + processCertificateIssuanceJobs + }; +}; 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 a7292e366..2b7c155db 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"; @@ -69,6 +67,7 @@ import { TGetCaDTO, TImportCertToCaDTO, TIssueCertFromCaDTO, + TIssueCertFromCaResponse, TRenewCaCertDTO, TSignCertFromCaDTO, TSignIntermediateDTO, @@ -127,6 +126,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 +155,6 @@ export const internalCertificateAuthorityServiceFactory = ({ notAfter, maxPathLength, keyAlgorithm, - enableDirectIssuance, name, ...dto }: TCreateCaDTO) => { @@ -192,9 +206,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 +368,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 +389,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 +985,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, @@ -1185,7 +1199,7 @@ export const internalCertificateAuthorityServiceFactory = ({ isFromProfile, internal = false, tx - }: TIssueCertFromCaDTO) => { + }: TIssueCertFromCaDTO): Promise => { let ca: TCertificateAuthorityWithAssociatedCa | undefined; let certificateTemplate: TCertificateTemplates | undefined; let collectionId = pkiCollectionId; @@ -1302,26 +1316,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 +1347,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), @@ -1538,10 +1533,11 @@ export const internalCertificateAuthorityServiceFactory = ({ return cert; }; + let cert; if (tx) { - await executeIssueCertOperations(tx); + cert = await executeIssueCertOperations(tx); } else { - await certificateDAL.transaction(executeIssueCertOperations); + cert = await certificateDAL.transaction(executeIssueCertOperations); } return { @@ -1550,6 +1546,8 @@ export const internalCertificateAuthorityServiceFactory = ({ issuingCaCertificate, privateKey: skLeaf, serialNumber, + certificateId: cert.id, + commonName, ca: expandInternalCa(ca) }; }; @@ -1577,15 +1575,16 @@ export const internalCertificateAuthorityServiceFactory = ({ keyUsages, extendedKeyUsages, signatureAlgorithm, - keyAlgorithm + keyAlgorithm, + tx } = dto; let collectionId = pkiCollectionId; if (caId) { - ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx); } else if (certificateTemplateId) { - certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId); + certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId, tx); if (!certificateTemplate) { throw new NotFoundError({ message: `Certificate template with ID '${certificateTemplateId}' not found` @@ -1593,7 +1592,7 @@ export const internalCertificateAuthorityServiceFactory = ({ } collectionId = certificateTemplate.pkiCollectionId as string; - ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId); + ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId, tx); } if (!ca) { @@ -1642,7 +1641,7 @@ export const internalCertificateAuthorityServiceFactory = ({ // check PKI collection if (pkiCollectionId) { - const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId); + const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId, tx); if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${pkiCollectionId}' not found` }); if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" }); } @@ -1690,22 +1689,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; @@ -1716,12 +1700,7 @@ export const internalCertificateAuthorityServiceFactory = ({ const csrObj = new x509.Pkcs10CertificateRequest(csr); const dn = parseDistinguishedName(csrObj.subject); - const cn = commonName || dn.commonName; - - if (!cn) - throw new BadRequestError({ - message: "A common name (CN) is required in the CSR or as a parameter to this endpoint" - }); + const cn = (commonName || dn.commonName) ?? ""; const { caPrivateKey, caSecret } = await getCaCredentials({ caId: ca.id, @@ -1733,9 +1712,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), @@ -1931,8 +1910,8 @@ export const internalCertificateAuthorityServiceFactory = ({ plainText: Buffer.from(certificateChainPem) }); - await certificateDAL.transaction(async (tx) => { - const cert = await certificateDAL.create( + const createSignedCert = async (transaction: Knex) => { + const newCert = await certificateDAL.create( { caId: (ca as TCertificateAuthorities).id, caCertId: caCert.id, @@ -1950,36 +1929,44 @@ export const internalCertificateAuthorityServiceFactory = ({ keyAlgorithm: keyAlgorithm || ca!.internalCa!.keyAlgorithm, signatureAlgorithm: signatureAlgorithm || ca!.internalCa!.keyAlgorithm }, - tx + transaction ); await certificateBodyDAL.create( { - certId: cert.id, + certId: newCert.id, encryptedCertificate, encryptedCertificateChain }, - tx + transaction ); if (collectionId) { await pkiCollectionItemDAL.create( { pkiCollectionId: collectionId, - certId: cert.id + certId: newCert.id }, - tx + transaction ); } - return cert; - }); + return newCert; + }; + + let cert; + if (tx) { + cert = await createSignedCert(tx); + } else { + cert = await certificateDAL.transaction(createSignedCert); + } return { certificate: leafCert, certificateChain: certificateChainPem, issuingCaCertificate, serialNumber, + certificateId: cert.id, ca: expandInternalCa(ca), commonName: cn }; 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..c13f85aa5 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 = { @@ -164,6 +160,7 @@ export type TSignCertFromCaDTO = keyAlgorithm?: string; isFromProfile?: boolean; profileId?: string; + tx?: Knex; } | ({ isInternal: false; @@ -183,6 +180,7 @@ export type TSignCertFromCaDTO = keyAlgorithm?: string; isFromProfile?: boolean; profileId?: string; + tx?: Knex; } & Omit); export type TGetCaCertificateTemplatesDTO = { @@ -252,3 +250,20 @@ export type TIssueCertWithTemplateDTO = { keyUsages?: CertKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[]; }; + +type TCaReference = { + id: string; + projectId: string; + dn: string; +}; + +export type TIssueCertFromCaResponse = { + certificate: string; + certificateChain: string; + issuingCaCertificate: string; + privateKey: string; + serialNumber: string; + certificateId: string; + ca: TCaReference; + commonName: string; +}; diff --git a/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts b/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts index 0d8ef30d0..2499a18d2 100644 --- a/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts +++ b/backend/src/services/certificate-est-v3/certificate-est-v3-service.ts @@ -67,6 +67,12 @@ export const certificateEstV3ServiceFactory = ({ throw new BadRequestError({ message: "EST enrollment not configured for this profile" }); } + if (!profile.caId) { + throw new BadRequestError({ + message: "Self-signed certificates are not supported for EST enrollment" + }); + } + const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId); if (!estConfig) { throw new NotFoundError({ message: "EST configuration not found" }); @@ -169,6 +175,12 @@ export const certificateEstV3ServiceFactory = ({ throw new BadRequestError({ message: "EST enrollment not configured for this profile" }); } + if (!profile.caId) { + throw new BadRequestError({ + message: "Self-signed certificates are not supported for EST enrollment" + }); + } + const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId); if (!estConfig) { throw new NotFoundError({ message: "EST configuration not found" }); @@ -281,6 +293,12 @@ export const certificateEstV3ServiceFactory = ({ throw new BadRequestError({ message: "EST enrollment not configured for this profile" }); } + if (!profile.caId) { + throw new BadRequestError({ + message: "Self-signed certificates are not supported for EST enrollment" + }); + } + const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId); if (!estConfig) { throw new NotFoundError({ message: "EST configuration not found" }); diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index d2f468248..7a6494d34 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -7,6 +7,7 @@ import { ormify, selectAllTableCols } from "@app/lib/knex"; import { EnrollmentType, + IssuerType, TCertificateProfile, TCertificateProfileCertificate, TCertificateProfileInsert, @@ -21,10 +22,19 @@ export const certificateProfileDALFactory = (db: TDbClient) => { const create = async (data: TCertificateProfileInsert, tx?: Knex): Promise => { try { - const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile).insert(data).returning("*")) as [ - TCertificateProfile - ]; - return certificateProfile; + const dataToInsert = { + ...data, + externalConfigs: data.externalConfigs ? JSON.stringify(data.externalConfigs) : null + }; + + const [insertedProfile] = await (tx || db)(TableName.PkiCertificateProfile).insert(dataToInsert).returning("*"); + + return { + ...insertedProfile, + externalConfigs: insertedProfile.externalConfigs + ? (JSON.parse(insertedProfile.externalConfigs) as Record) + : null + } as TCertificateProfile; } catch (error) { throw new DatabaseError({ error, name: "Create certificate profile" }); } @@ -32,11 +42,25 @@ export const certificateProfileDALFactory = (db: TDbClient) => { const updateById = async (id: string, data: TCertificateProfileUpdate, tx?: Knex): Promise => { try { - const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile) + const dataToUpdate: Partial> = { + ...data + }; + + if (data.externalConfigs !== undefined) { + dataToUpdate.externalConfigs = data.externalConfigs ? JSON.stringify(data.externalConfigs) : null; + } + + const [updatedProfile] = await (tx || db)(TableName.PkiCertificateProfile) .where({ id }) - .update(data) - .returning("*")) as [TCertificateProfile]; - return certificateProfile; + .update(dataToUpdate) + .returning("*"); + + return { + ...updatedProfile, + externalConfigs: updatedProfile.externalConfigs + ? (JSON.parse(updatedProfile.externalConfigs) as Record) + : null + } as TCertificateProfile; } catch (error) { throw new DatabaseError({ error, name: "Update certificate profile" }); } @@ -56,10 +80,16 @@ export const certificateProfileDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex): Promise => { try { - const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile).where({ id }).first()) as - | TCertificateProfile - | undefined; - return certificateProfile; + const certificateProfile = await (tx || db)(TableName.PkiCertificateProfile).where({ id }).first(); + + if (!certificateProfile) return undefined; + + return { + ...certificateProfile, + externalConfigs: certificateProfile.externalConfigs + ? (JSON.parse(certificateProfile.externalConfigs) as Record) + : null + } as TCertificateProfile; } catch (error) { throw new DatabaseError({ error, name: "Find certificate profile by id" }); } @@ -198,9 +228,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => { slug: result.slug, description: result.description, enrollmentType: result.enrollmentType as EnrollmentType, + issuerType: result.issuerType as IssuerType, estConfigId: result.estConfigId, apiConfigId: result.apiConfigId, acmeConfigId: result.acmeConfigId, + externalConfigs: result.externalConfigs + ? (JSON.parse(result.externalConfigs) as Record) + : null, createdAt: result.createdAt, updatedAt: result.updatedAt, estConfig, @@ -239,12 +273,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => { limit?: number; search?: string; enrollmentType?: EnrollmentType; + issuerType?: IssuerType; caId?: string; } = {}, tx?: Knex ): Promise => { try { - const { offset = 0, limit = 20, search, enrollmentType, caId } = options; + const { offset = 0, limit = 20, search, enrollmentType, issuerType, caId } = options; let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where( `${TableName.PkiCertificateProfile}.projectId`, @@ -269,7 +304,21 @@ export const certificateProfileDALFactory = (db: TDbClient) => { baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId); } + if (issuerType) { + baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.issuerType`, issuerType); + } + const query = baseQuery + .leftJoin( + TableName.CertificateAuthority, + `${TableName.PkiCertificateProfile}.caId`, + `${TableName.CertificateAuthority}.id` + ) + .leftJoin( + TableName.ExternalCertificateAuthority, + `${TableName.CertificateAuthority}.id`, + `${TableName.ExternalCertificateAuthority}.caId` + ) .leftJoin( TableName.PkiEstEnrollmentConfig, `${TableName.PkiCertificateProfile}.estConfigId`, @@ -287,6 +336,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => { ) .select(selectAllTableCols(TableName.PkiCertificateProfile)) .select( + db.ref("id").withSchema(TableName.CertificateAuthority).as("caId"), + db.ref("name").withSchema(TableName.CertificateAuthority).as("caName"), + db.ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"), + db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"), + db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalCaType"), db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"), db .ref("disableBootstrapCaValidation") @@ -330,6 +384,16 @@ export const certificateProfileDALFactory = (db: TDbClient) => { } : undefined; + const certificateAuthority = result.caId + ? { + id: result.caId as string, + name: result.caName as string, + status: result.caStatus as string, + isExternal: !!result.externalCaId, + externalType: result.externalCaType as string | undefined + } + : undefined; + const baseProfile = { id: result.id, projectId: result.projectId, @@ -338,13 +402,19 @@ export const certificateProfileDALFactory = (db: TDbClient) => { slug: result.slug, description: result.description, enrollmentType: result.enrollmentType as EnrollmentType, + issuerType: result.issuerType as IssuerType, estConfigId: result.estConfigId, apiConfigId: result.apiConfigId, + acmeConfigId: result.acmeConfigId, + externalConfigs: result.externalConfigs + ? (JSON.parse(result.externalConfigs as string) as Record) + : null, createdAt: result.createdAt, updatedAt: result.updatedAt, estConfig, apiConfig, - acmeConfig + acmeConfig, + certificateAuthority }; return baseProfile as TCertificateProfileWithConfigs; @@ -359,12 +429,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => { options: { search?: string; enrollmentType?: EnrollmentType; + issuerType?: IssuerType; caId?: string; } = {}, tx?: Knex ): Promise => { try { - const { search, enrollmentType, caId } = options; + const { search, enrollmentType, issuerType, caId } = options; let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId }); @@ -384,6 +455,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => { query = query.where({ caId }); } + if (issuerType) { + query = query.where({ issuerType }); + } + const result = await query.count("*").first(); return parseInt((result as unknown as { count: string }).count || "0", 10); } catch (error) { diff --git a/backend/src/services/certificate-profile/certificate-profile-external-config-schemas.ts b/backend/src/services/certificate-profile/certificate-profile-external-config-schemas.ts new file mode 100644 index 000000000..2d54d0d4f --- /dev/null +++ b/backend/src/services/certificate-profile/certificate-profile-external-config-schemas.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; + +/** + * External configuration schema for Azure AD CS Certificate Authority + */ +export const AzureAdCsExternalConfigSchema = z.object({ + template: z + .string() + .min(1, "Template name is required for Azure AD CS") + .describe("Certificate template name for Azure AD CS") +}); + +/** + * External configuration schema for ACME Certificate Authority + */ +export const AcmeExternalConfigSchema = z.object({}); + +/** + * Map of CA types to their corresponding external configuration schemas + */ +export const ExternalConfigSchemaMap = { + [CaType.AZURE_AD_CS]: AzureAdCsExternalConfigSchema, + [CaType.ACME]: AcmeExternalConfigSchema, + [CaType.INTERNAL]: z.object({}).optional() // Internal CAs don't use external configs +} as const; + +export const createExternalConfigSchema = (caType?: CaType | null) => { + if (!caType || caType === CaType.INTERNAL) { + return z.object({}).nullable().optional(); + } + + const schema = ExternalConfigSchemaMap[caType]; + if (!schema) { + return z.object({}).nullable().optional(); + } + + return schema.nullable().optional(); +}; + +/** + * Union type of all possible external configuration schemas + */ +export const ExternalConfigUnionSchema = z + .union([AzureAdCsExternalConfigSchema, AcmeExternalConfigSchema, z.object({})]) + .nullable() + .optional(); + +export type TExternalConfig = z.infer; diff --git a/backend/src/services/certificate-profile/certificate-profile-schemas.ts b/backend/src/services/certificate-profile/certificate-profile-schemas.ts index bf88593bd..e6b574dea 100644 --- a/backend/src/services/certificate-profile/certificate-profile-schemas.ts +++ b/backend/src/services/certificate-profile/certificate-profile-schemas.ts @@ -1,12 +1,13 @@ import RE2 from "re2"; import { z } from "zod"; -import { EnrollmentType } from "./certificate-profile-types"; +import { CertStatus } from "../certificate/certificate-types"; +import { EnrollmentType, IssuerType } from "./certificate-profile-types"; export const createCertificateProfileSchema = z .object({ projectId: z.string().uuid("Project ID must be valid"), - caId: z.string().uuid(), + caId: z.string().uuid().nullable().optional(), certificateTemplateId: z.string().uuid(), slug: z .string() @@ -15,6 +16,7 @@ export const createCertificateProfileSchema = z .regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"), description: z.string().max(1000).optional(), enrollmentType: z.nativeEnum(EnrollmentType), + issuerType: z.nativeEnum(IssuerType).default(IssuerType.CA), estConfig: z .object({ disableBootstrapCaValidation: z.boolean().default(false), @@ -33,43 +35,100 @@ export const createCertificateProfileSchema = z .refine( (data) => { if (data.enrollmentType === EnrollmentType.EST) { - if (!data.estConfig) { - return false; - } - if (data.apiConfig) { - return false; - } - if (data.acmeConfig) { - return false; - } - } - if (data.enrollmentType === EnrollmentType.API) { - if (!data.apiConfig) { - return false; - } - if (data.estConfig) { - return false; - } - if (data.acmeConfig) { - return false; - } - } - if (data.enrollmentType === EnrollmentType.ACME) { - if (!data.acmeConfig) { - return false; - } - if (data.estConfig) { - return false; - } - if (data.apiConfig) { - return false; - } + return !!data.estConfig; } return true; }, { - message: - "EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration." + message: "EST enrollment type requires EST configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.API) { + return !!data.apiConfig; + } + return true; + }, + { + message: "API enrollment type requires API configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.ACME) { + return !!data.acmeConfig; + } + return true; + }, + { + message: "ACME enrollment type requires ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.EST) { + return !data.apiConfig && !data.acmeConfig; + } + return true; + }, + { + message: "EST enrollment type cannot have API or ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.API) { + return !data.estConfig && !data.acmeConfig; + } + return true; + }, + { + message: "API enrollment type cannot have EST or ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.ACME) { + return !data.estConfig && !data.apiConfig; + } + return true; + }, + { + message: "ACME enrollment type cannot have EST or API configuration" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.CA) { + return !!data.caId; + } + return true; + }, + { + message: "CA issuer type requires a CA ID" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.SELF_SIGNED) { + return !data.caId; + } + return true; + }, + { + message: "Self-signed issuer type cannot have a CA ID" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.SELF_SIGNED) { + return data.enrollmentType === EnrollmentType.API; + } + return true; + }, + { + message: "Self-signed issuer type only supports API enrollment" } ); @@ -83,6 +142,7 @@ export const updateCertificateProfileSchema = z .optional(), description: z.string().max(1000).optional(), enrollmentType: z.nativeEnum(EnrollmentType).optional(), + issuerType: z.nativeEnum(IssuerType).optional(), estConfig: z .object({ disableBootstrapCaValidation: z.boolean().default(false), @@ -100,19 +160,34 @@ export const updateCertificateProfileSchema = z .refine( (data) => { if (data.enrollmentType === EnrollmentType.EST) { - if (data.apiConfig) { - return false; - } - } - if (data.enrollmentType === EnrollmentType.API) { - if (data.estConfig) { - return false; - } + return !data.apiConfig; } return true; }, { - message: "Cannot have EST config with API enrollment type or API config with EST enrollment type." + message: "EST enrollment type cannot have API configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.API) { + return !data.estConfig; + } + return true; + }, + { + message: "API enrollment type cannot have EST configuration" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.SELF_SIGNED) { + return !data.enrollmentType || data.enrollmentType === EnrollmentType.API; + } + return true; + }, + { + message: "Self-signed issuer type only supports API enrollment" } ); @@ -131,6 +206,7 @@ export const listCertificateProfilesSchema = z.object({ limit: z.coerce.number().min(1).max(100).default(20), search: z.string().optional(), enrollmentType: z.nativeEnum(EnrollmentType).optional(), + issuerType: z.nativeEnum(IssuerType).optional(), caId: z.string().uuid().optional() }); @@ -142,6 +218,6 @@ export const listCertificatesByProfileSchema = z.object({ profileId: z.string().uuid(), offset: z.coerce.number().min(0).default(0), limit: z.coerce.number().min(1).max(100).default(20), - status: z.enum(["active", "expired", "revoked"]).optional(), + status: z.nativeEnum(CertStatus).optional(), search: z.string().optional() }); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index 3b75c1088..17ddcaa30 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -12,8 +12,8 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { ActorType, AuthMethod } from "../auth/auth-type"; import type { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; import type { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; -import type { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal"; import type { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; +import type { TExternalCertificateAuthorityDALFactory } from "../certificate-authority/external-certificate-authority-dal"; import type { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal"; import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal"; import type { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal"; @@ -22,7 +22,12 @@ import type { TKmsServiceFactory } from "../kms/kms-service"; import type { TProjectDALFactory } from "../project/project-dal"; import type { TCertificateProfileDALFactory } from "./certificate-profile-dal"; import { certificateProfileServiceFactory, TCertificateProfileServiceFactory } from "./certificate-profile-service"; -import { EnrollmentType, TCertificateProfile, TCertificateProfileWithConfigs } from "./certificate-profile-types"; +import { + EnrollmentType, + IssuerType, + TCertificateProfile, + TCertificateProfileWithConfigs +} from "./certificate-profile-types"; vi.mock("@app/lib/crypto/cryptography", () => ({ crypto: { @@ -90,10 +95,12 @@ describe("CertificateProfileService", () => { description: "Test certificate profile", slug: "test-profile", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", apiConfigId: "api-config-123", estConfigId: null, + externalConfigs: null, createdAt: new Date(), updatedAt: new Date() }; @@ -223,17 +230,10 @@ describe("CertificateProfileService", () => { delete: vi.fn() } as unknown as TCertificateAuthorityDALFactory; - const mockCertificateAuthorityCertDAL = { - create: vi.fn(), + const mockExternalCertificateAuthorityDAL = { findById: vi.fn(), - updateById: vi.fn(), - deleteById: vi.fn(), - transaction: vi.fn(), - find: vi.fn(), - findOne: vi.fn(), - update: vi.fn(), - delete: vi.fn() - } as unknown as TCertificateAuthorityCertDALFactory; + findOne: vi.fn() + } as unknown as Pick; beforeEach(() => { vi.spyOn(ForbiddenError, "from").mockReturnValue({ @@ -255,7 +255,7 @@ describe("CertificateProfileService", () => { certificateBodyDAL: mockCertificateBodyDAL, certificateSecretDAL: mockCertificateSecretDAL, certificateAuthorityDAL: mockCertificateAuthorityDAL, - certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL, + externalCertificateAuthorityDAL: mockExternalCertificateAuthorityDAL, permissionService: mockPermissionService, licenseService: mockLicenseService, kmsService: mockKmsService, @@ -272,6 +272,7 @@ describe("CertificateProfileService", () => { slug: "new-profile", description: "New test profile", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", apiConfig: { @@ -312,6 +313,7 @@ describe("CertificateProfileService", () => { slug: "new-profile", description: "New test profile", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", apiConfigId: "api-config-123", @@ -383,6 +385,7 @@ describe("CertificateProfileService", () => { slug: "invalid-profile", description: "Invalid test profile", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123" }; @@ -401,6 +404,7 @@ describe("CertificateProfileService", () => { slug: "api-profile", description: "Profile with API enrollment", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", apiConfig: { @@ -726,6 +730,7 @@ describe("CertificateProfileService", () => { slug: "est-profile", description: "Profile with EST enrollment", enrollmentType: EnrollmentType.EST, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", estConfig: { @@ -776,6 +781,7 @@ describe("CertificateProfileService", () => { slug: "different-profile-name", description: "Profile with duplicate slug", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", apiConfig: { @@ -801,6 +807,7 @@ describe("CertificateProfileService", () => { slug: "auto-renew-profile", description: "Profile with auto-renewal", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", apiConfig: { @@ -965,6 +972,7 @@ describe("CertificateProfileService", () => { slug: "invalid-template-profile", description: "Profile with invalid template", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "nonexistent-template", apiConfig: { @@ -990,6 +998,7 @@ describe("CertificateProfileService", () => { slug: "concurrent-profile", description: "Profile created concurrently", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", apiConfig: { @@ -1018,6 +1027,7 @@ describe("CertificateProfileService", () => { slug: "cross-project-profile", description: "Profile using template from different project", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-456", apiConfig: { @@ -1047,6 +1057,7 @@ describe("CertificateProfileService", () => { slug: "invalid-slug-profile", description: "Profile with invalid slug format", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", apiConfig: { diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 12e272ad6..a771b7b0a 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -19,8 +19,9 @@ import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"; import { getCertificateCredentials, isCertChainValid } from "../certificate/certificate-fns"; import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; -import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; +import { CaType } from "../certificate-authority/certificate-authority-enums"; +import { TExternalCertificateAuthorityDALFactory } from "../certificate-authority/external-certificate-authority-dal"; import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal"; import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal"; import { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal"; @@ -32,6 +33,7 @@ import { getProjectKmsCertificateKeyId } from "../project/project-fns"; import { TCertificateProfileDALFactory } from "./certificate-profile-dal"; import { EnrollmentType, + IssuerType, TCertificateProfile, TCertificateProfileCertificate, TCertificateProfileInsert, @@ -39,6 +41,83 @@ import { TCertificateProfileWithConfigs } from "./certificate-profile-types"; +const validateIssuerTypeConstraints = ( + issuerType: IssuerType, + enrollmentType: EnrollmentType, + caId: string | null, + existingCaId?: string | null +) => { + if (issuerType === IssuerType.CA) { + if (!caId && !existingCaId) { + throw new ForbiddenRequestError({ + message: "CA issuer type requires a Certificate Authority to be selected" + }); + } + } + + if (issuerType === IssuerType.SELF_SIGNED) { + if (caId) { + throw new ForbiddenRequestError({ + message: "Self-signed issuer type cannot have a Certificate Authority" + }); + } + if (enrollmentType !== EnrollmentType.API) { + throw new ForbiddenRequestError({ + message: "Self-signed issuer type only supports API enrollment" + }); + } + } +}; + +const validateTemplateByExternalCaType = ( + externalCaType: CaType | undefined, + externalConfigs: Record | null | undefined +) => { + if (!externalCaType) return; + + switch (externalCaType) { + case CaType.AZURE_AD_CS: + if (!externalConfigs?.template || typeof externalConfigs.template !== "string") { + throw new ForbiddenRequestError({ + message: "Azure ADCS Certificate Authority requires a template to be specified in external configs" + }); + } + break; + default: + break; + } +}; + +const validateExternalConfigs = async ( + externalConfigs: Record | null | undefined, + caId: string | null, + certificateAuthorityDAL: Pick, + externalCertificateAuthorityDAL: Pick +) => { + if (!externalConfigs) return; + + if (!caId) { + throw new ForbiddenRequestError({ + message: "External configs can only be specified when a Certificate Authority is selected" + }); + } + + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) { + throw new NotFoundError({ message: "Certificate Authority not found" }); + } + + const externalCa = await externalCertificateAuthorityDAL.findOne({ caId }); + + if (!externalCa) { + throw new ForbiddenRequestError({ + message: "External configs can only be specified for external Certificate Authorities" + }); + } + + validateTemplateByExternalCaType(externalCa.type as CaType, externalConfigs); +}; + const generateAndEncryptAcmeEabSecret = async ( projectId: string, kmsService: Pick, @@ -151,7 +230,7 @@ type TCertificateProfileServiceFactoryDep = { certificateBodyDAL: Pick; certificateSecretDAL: Pick; certificateAuthorityDAL: Pick; - certificateAuthorityCertDAL: Pick; + externalCertificateAuthorityDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; @@ -161,9 +240,22 @@ type TCertificateProfileServiceFactoryDep = { export type TCertificateProfileServiceFactory = ReturnType; const convertDalToService = (dalResult: Record): TCertificateProfile => { + let parsedExternalConfigs: Record | null = null; + if (dalResult.externalConfigs && typeof dalResult.externalConfigs === "string") { + try { + parsedExternalConfigs = JSON.parse(dalResult.externalConfigs) as Record; + } catch { + parsedExternalConfigs = null; + } + } else if (dalResult.externalConfigs && typeof dalResult.externalConfigs === "object") { + parsedExternalConfigs = dalResult.externalConfigs as Record; + } + return { ...dalResult, - enrollmentType: dalResult.enrollmentType as EnrollmentType + enrollmentType: dalResult.enrollmentType as EnrollmentType, + issuerType: dalResult.issuerType as IssuerType, + externalConfigs: parsedExternalConfigs } as TCertificateProfile; }; @@ -175,6 +267,8 @@ export const certificateProfileServiceFactory = ({ acmeEnrollmentConfigDAL, certificateBodyDAL, certificateSecretDAL, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, permissionService, licenseService, kmsService, @@ -240,6 +334,16 @@ export const certificateProfileServiceFactory = ({ }); } + validateIssuerTypeConstraints(data.issuerType, data.enrollmentType, data.caId ?? null); + + // Validate external configs + await validateExternalConfigs( + data.externalConfigs, + data.caId ?? null, + certificateAuthorityDAL, + externalCertificateAuthorityDAL + ); + // Validate enrollment configuration requirements if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) { throw new ForbiddenRequestError({ @@ -308,7 +412,8 @@ export const certificateProfileServiceFactory = ({ projectId, estConfigId, apiConfigId, - acmeConfigId + acmeConfigId, + externalConfigs: data.externalConfigs }, tx ); @@ -376,7 +481,26 @@ export const certificateProfileServiceFactory = ({ } } - const { estConfig, apiConfig, ...profileUpdateData } = data; + const finalIssuerType = data.issuerType || existingProfile.issuerType; + const finalEnrollmentType = data.enrollmentType || existingProfile.enrollmentType; + const finalCaId = data.caId !== undefined ? data.caId : existingProfile.caId; + + validateIssuerTypeConstraints(finalIssuerType, finalEnrollmentType, finalCaId ?? null, existingProfile.caId); + + // Validate external configs only if they are provided in the update + if (data.externalConfigs !== undefined) { + await validateExternalConfigs( + data.externalConfigs, + finalCaId ?? null, + certificateAuthorityDAL, + externalCertificateAuthorityDAL + ); + } + + const updatedData = + finalIssuerType === IssuerType.SELF_SIGNED && existingProfile.caId ? { ...data, caId: null } : data; + + const { estConfig, apiConfig, ...profileUpdateData } = updatedData; const updatedProfile = await certificateProfileDAL.transaction(async (tx) => { if (estConfig && existingProfile.estConfigId) { @@ -517,9 +641,24 @@ export const certificateProfileServiceFactory = ({ } } + // Parse externalConfigs from JSON string to object if it exists + let parsedExternalConfigs: Record | null = null; + if (profile.externalConfigs && typeof profile.externalConfigs === "string") { + try { + parsedExternalConfigs = JSON.parse(profile.externalConfigs) as Record; + } catch { + // If parsing fails, leave as null + parsedExternalConfigs = null; + } + } else if (profile.externalConfigs && typeof profile.externalConfigs === "object") { + // Already an object, use as-is + parsedExternalConfigs = profile.externalConfigs; + } + return { ...profile, - enrollmentType: profile.enrollmentType as EnrollmentType + enrollmentType: profile.enrollmentType as EnrollmentType, + externalConfigs: parsedExternalConfigs }; }; @@ -569,6 +708,7 @@ export const certificateProfileServiceFactory = ({ limit = 20, search, enrollmentType, + issuerType, caId }: { actor: ActorType; @@ -580,6 +720,7 @@ export const certificateProfileServiceFactory = ({ limit?: number; search?: string; enrollmentType?: EnrollmentType; + issuerType?: IssuerType; caId?: string; }): Promise<{ profiles: TCertificateProfileWithConfigs[]; @@ -603,12 +744,14 @@ export const certificateProfileServiceFactory = ({ limit, search, enrollmentType, + issuerType, caId }); const totalCount = await certificateProfileDAL.countByProjectId(projectId, { search, enrollmentType, + issuerType, caId }); diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index 030548e97..3eca249cd 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -10,16 +10,33 @@ export enum EnrollmentType { ACME = "acme" } -export type TCertificateProfile = Omit & { +export enum IssuerType { + CA = "ca", + SELF_SIGNED = "self-signed" +} + +export type TCertificateProfile = Omit & { enrollmentType: EnrollmentType; + issuerType: IssuerType; + externalConfigs?: Record | null; }; -export type TCertificateProfileInsert = Omit & { +export type TCertificateProfileInsert = Omit< + TPkiCertificateProfilesInsert, + "enrollmentType" | "issuerType" | "externalConfigs" +> & { enrollmentType: EnrollmentType; + issuerType: IssuerType; + externalConfigs?: Record | null; }; -export type TCertificateProfileUpdate = Omit & { +export type TCertificateProfileUpdate = Omit< + TPkiCertificateProfilesUpdate, + "enrollmentType" | "issuerType" | "externalConfigs" +> & { enrollmentType?: EnrollmentType; + issuerType?: IssuerType; + externalConfigs?: Record | null; estConfig?: { disableBootstrapCaValidation?: boolean; passphrase?: string; @@ -42,6 +59,8 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & { projectId: string; status: string; name: string; + isExternal?: boolean; + externalType?: string; }; certificateTemplate?: { id: string; diff --git a/backend/src/services/certificate-request/certificate-request-dal.ts b/backend/src/services/certificate-request/certificate-request-dal.ts new file mode 100644 index 000000000..df2a4b0c4 --- /dev/null +++ b/backend/src/services/certificate-request/certificate-request-dal.ts @@ -0,0 +1,92 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName, TCertificateRequests, TCertificates } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +type TCertificateRequestWithCertificate = TCertificateRequests & { + certificate: TCertificates | null; +}; + +export type TCertificateRequestDALFactory = ReturnType; + +export const certificateRequestDALFactory = (db: TDbClient) => { + const certificateRequestOrm = ormify(db, TableName.CertificateRequests); + + const findByIdWithCertificate = async (id: string): Promise => { + try { + const certificateRequest = await certificateRequestOrm.findById(id); + if (!certificateRequest) return null; + + if (!certificateRequest.certificateId) { + return { + ...certificateRequest, + certificate: null + }; + } + + const certificate = await db(TableName.Certificate) + .where("id", certificateRequest.certificateId) + .select(selectAllTableCols(TableName.Certificate)) + .first(); + + return { + ...certificateRequest, + certificate: certificate || null + }; + } catch (error) { + throw new DatabaseError({ error, name: "Find certificate request by ID with certificate" }); + } + }; + + const findPendingByProjectId = async (projectId: string): Promise => { + try { + return (await db(TableName.CertificateRequests) + .where({ projectId, status: "pending" }) + .orderBy("createdAt", "desc")) as TCertificateRequests[]; + } catch (error) { + throw new DatabaseError({ error, name: "Find pending certificate requests by project ID" }); + } + }; + + const updateStatus = async ( + id: string, + status: string, + errorMessage?: string, + tx?: Knex + ): Promise => { + try { + const updateData: Partial = { status }; + if (errorMessage !== undefined) { + updateData.errorMessage = errorMessage; + } + return await certificateRequestOrm.updateById(id, updateData, tx); + } catch (error) { + throw new DatabaseError({ error, name: "Update certificate request status" }); + } + }; + + const attachCertificate = async (id: string, certificateId: string, tx?: Knex): Promise => { + try { + return await certificateRequestOrm.updateById( + id, + { + certificateId, + status: "issued" + }, + tx + ); + } catch (error) { + throw new DatabaseError({ error, name: "Attach certificate to request" }); + } + }; + + return { + ...certificateRequestOrm, + findByIdWithCertificate, + findPendingByProjectId, + updateStatus, + attachCertificate + }; +}; diff --git a/backend/src/services/certificate-request/certificate-request-service.test.ts b/backend/src/services/certificate-request/certificate-request-service.test.ts new file mode 100644 index 000000000..5e6b870bf --- /dev/null +++ b/backend/src/services/certificate-request/certificate-request-service.test.ts @@ -0,0 +1,563 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { createMongoAbility, ForbiddenError } from "@casl/ability"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { ActionProjectType } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionSet, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { NotFoundError } from "@app/lib/errors"; +import { ActorType, AuthMethod } from "@app/services/auth/auth-type"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; + +import { TCertificateRequestDALFactory } from "./certificate-request-dal"; +import { certificateRequestServiceFactory, TCertificateRequestServiceFactory } from "./certificate-request-service"; +import { CertificateRequestStatus } from "./certificate-request-types"; + +describe("CertificateRequestService", () => { + let service: TCertificateRequestServiceFactory; + + const mockCertificateRequestDAL: Pick< + TCertificateRequestDALFactory, + "create" | "findById" | "findByIdWithCertificate" | "updateStatus" | "attachCertificate" + > = { + create: vi.fn() as any, + findById: vi.fn() as any, + findByIdWithCertificate: vi.fn() as any, + updateStatus: vi.fn() as any, + attachCertificate: vi.fn() as any + }; + + const mockCertificateDAL: Pick = { + findById: vi.fn() as any + }; + + const mockCertificateService: Pick = { + getCertBody: vi.fn() as any, + getCertPrivateKey: vi.fn() as any + }; + + const mockPermissionService: Pick = { + getProjectPermission: vi.fn() as any + }; + + beforeEach(() => { + vi.clearAllMocks(); + service = certificateRequestServiceFactory({ + certificateRequestDAL: mockCertificateRequestDAL as TCertificateRequestDALFactory, + certificateDAL: mockCertificateDAL, + certificateService: mockCertificateService, + permissionService: mockPermissionService + }); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe("createCertificateRequest", () => { + const mockCreateData = { + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002", + projectId: "550e8400-e29b-41d4-a716-446655440003", + profileId: "550e8400-e29b-41d4-a716-446655440004", + commonName: "test.example.com", + status: CertificateRequestStatus.PENDING + }; + + it("should create certificate request successfully", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Create, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + const mockCreatedRequest = { + id: "550e8400-e29b-41d4-a716-446655440005", + status: CertificateRequestStatus.PENDING, + projectId: "550e8400-e29b-41d4-a716-446655440003", + profileId: "550e8400-e29b-41d4-a716-446655440004", + commonName: "test.example.com" + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.create as any).mockResolvedValue(mockCreatedRequest); + + const result = await service.createCertificateRequest(mockCreateData); + + expect(mockPermissionService.getProjectPermission).toHaveBeenCalledWith({ + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + projectId: "550e8400-e29b-41d4-a716-446655440003", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002", + actionProjectType: ActionProjectType.CertificateManager + }); + expect(mockCertificateRequestDAL.create).toHaveBeenCalledWith( + { + status: CertificateRequestStatus.PENDING, + projectId: "550e8400-e29b-41d4-a716-446655440003", + profileId: "550e8400-e29b-41d4-a716-446655440004", + commonName: "test.example.com" + }, + undefined + ); + expect(result).toEqual(mockCreatedRequest); + }); + + it("should throw ForbiddenError when user lacks permission", async () => { + const mockPermission = { + permission: ForbiddenError.from(createMongoAbility([])) + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + + await expect(service.createCertificateRequest(mockCreateData)).rejects.toThrow(); + }); + }); + + describe("getCertificateRequest", () => { + const mockGetData = { + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002", + projectId: "550e8400-e29b-41d4-a716-446655440003", + certificateRequestId: "550e8400-e29b-41d4-a716-446655440005" + }; + + it("should get certificate request successfully", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Read, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + const mockRequest = { + id: "550e8400-e29b-41d4-a716-446655440005", + projectId: "550e8400-e29b-41d4-a716-446655440003", + status: CertificateRequestStatus.PENDING + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest); + + const result = await service.getCertificateRequest(mockGetData); + + expect(mockPermissionService.getProjectPermission).toHaveBeenCalledWith({ + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + projectId: "550e8400-e29b-41d4-a716-446655440003", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002", + actionProjectType: ActionProjectType.CertificateManager + }); + expect(mockCertificateRequestDAL.findById).toHaveBeenCalledWith("550e8400-e29b-41d4-a716-446655440005"); + expect(result).toEqual(mockRequest); + }); + + it("should throw NotFoundError when certificate request does not exist", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Read, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.findById as any).mockResolvedValue(null); + + await expect(service.getCertificateRequest(mockGetData)).rejects.toThrow(NotFoundError); + }); + + it("should throw BadRequestError when certificate request belongs to different project", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Read, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + const mockRequest = { + id: "550e8400-e29b-41d4-a716-446655440005", + projectId: "550e8400-e29b-41d4-a716-446655440099", + status: CertificateRequestStatus.PENDING + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest); + + await expect(service.getCertificateRequest(mockGetData)).rejects.toThrow(NotFoundError); + }); + }); + + describe("getCertificateFromRequest", () => { + const mockGetData = { + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002", + projectId: "550e8400-e29b-41d4-a716-446655440003", + certificateRequestId: "550e8400-e29b-41d4-a716-446655440005" + }; + + it("should get certificate from request successfully when certificate is attached", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Read, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + const mockCertificate = { + id: "550e8400-e29b-41d4-a716-446655440006", + serialNumber: "123456", + commonName: "test.example.com" + }; + const mockRequestWithCert = { + id: "550e8400-e29b-41d4-a716-446655440005", + projectId: "550e8400-e29b-41d4-a716-446655440003", + status: CertificateRequestStatus.ISSUED, + certificate: mockCertificate, + errorMessage: null, + createdAt: new Date(), + updatedAt: new Date() + }; + const mockCertBody = { + certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----" + }; + const mockPrivateKey = { + certPrivateKey: "-----BEGIN PRIVATE KEY-----\nMOCK_KEY_PEM\n-----END PRIVATE KEY-----" + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert); + (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); + (mockCertificateService.getCertPrivateKey as any).mockResolvedValue(mockPrivateKey); + + const result = await service.getCertificateFromRequest(mockGetData); + + expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith( + "550e8400-e29b-41d4-a716-446655440005" + ); + expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({ + id: "550e8400-e29b-41d4-a716-446655440006", + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002" + }); + expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({ + id: "550e8400-e29b-41d4-a716-446655440006", + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002" + }); + expect(result).toEqual({ + status: CertificateRequestStatus.ISSUED, + certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----", + privateKey: "-----BEGIN PRIVATE KEY-----\nMOCK_KEY_PEM\n-----END PRIVATE KEY-----", + serialNumber: "123456", + errorMessage: null, + createdAt: mockRequestWithCert.createdAt, + updatedAt: mockRequestWithCert.updatedAt + }); + }); + + it("should get certificate from request successfully when no certificate is attached", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Read, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + const mockRequestWithoutCert = { + id: "550e8400-e29b-41d4-a716-446655440007", + projectId: "550e8400-e29b-41d4-a716-446655440003", + status: CertificateRequestStatus.PENDING, + certificate: null, + errorMessage: null, + createdAt: new Date(), + updatedAt: new Date() + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithoutCert); + + const result = await service.getCertificateFromRequest(mockGetData); + + expect(result).toEqual({ + status: CertificateRequestStatus.PENDING, + certificate: null, + privateKey: null, + serialNumber: null, + errorMessage: null, + createdAt: mockRequestWithoutCert.createdAt, + updatedAt: mockRequestWithoutCert.updatedAt + }); + }); + + it("should get certificate from request successfully when private key access is denied", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Read, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + const mockCertificate = { + id: "550e8400-e29b-41d4-a716-446655440008", + serialNumber: "123456", + commonName: "test.example.com" + }; + const mockRequestWithCert = { + id: "550e8400-e29b-41d4-a716-446655440005", + projectId: "550e8400-e29b-41d4-a716-446655440003", + status: CertificateRequestStatus.ISSUED, + certificate: mockCertificate, + errorMessage: null, + createdAt: new Date(), + updatedAt: new Date() + }; + const mockCertBody = { + certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----" + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert); + (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); + (mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key access denied")); + + const result = await service.getCertificateFromRequest(mockGetData); + + expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith( + "550e8400-e29b-41d4-a716-446655440005" + ); + expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({ + id: "550e8400-e29b-41d4-a716-446655440008", + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002" + }); + expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({ + id: "550e8400-e29b-41d4-a716-446655440008", + actor: ActorType.USER, + actorId: "550e8400-e29b-41d4-a716-446655440001", + actorAuthMethod: AuthMethod.EMAIL, + actorOrgId: "550e8400-e29b-41d4-a716-446655440002" + }); + expect(result).toEqual({ + status: CertificateRequestStatus.ISSUED, + certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----", + privateKey: null, + serialNumber: "123456", + errorMessage: null, + createdAt: mockRequestWithCert.createdAt, + updatedAt: mockRequestWithCert.updatedAt + }); + }); + + it("should get certificate from request with error message when failed", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Read, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + const mockFailedRequest = { + id: "550e8400-e29b-41d4-a716-446655440010", + projectId: "550e8400-e29b-41d4-a716-446655440003", + status: CertificateRequestStatus.FAILED, + certificate: null, + errorMessage: "Certificate issuance failed", + createdAt: new Date(), + updatedAt: new Date() + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockFailedRequest); + + const result = await service.getCertificateFromRequest(mockGetData); + + expect(result).toEqual({ + status: CertificateRequestStatus.FAILED, + certificate: null, + privateKey: null, + serialNumber: null, + errorMessage: "Certificate issuance failed", + createdAt: mockFailedRequest.createdAt, + updatedAt: mockFailedRequest.updatedAt + }); + }); + + it("should throw NotFoundError when certificate request does not exist", async () => { + const mockPermission = { + permission: createMongoAbility([ + { + action: ProjectPermissionCertificateActions.Read, + subject: ProjectPermissionSub.Certificates + } + ]) + }; + + (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); + (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(null); + + await expect(service.getCertificateFromRequest(mockGetData)).rejects.toThrow(NotFoundError); + }); + }); + + describe("updateCertificateRequestStatus", () => { + it("should update certificate request status successfully", async () => { + const mockRequest = { + id: "550e8400-e29b-41d4-a716-446655440011", + status: CertificateRequestStatus.PENDING + }; + const mockUpdatedRequest = { + id: "550e8400-e29b-41d4-a716-446655440011", + status: CertificateRequestStatus.ISSUED + }; + + (mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest); + (mockCertificateRequestDAL.updateStatus as any).mockResolvedValue(mockUpdatedRequest); + + const result = await service.updateCertificateRequestStatus({ + certificateRequestId: "550e8400-e29b-41d4-a716-446655440011", + status: CertificateRequestStatus.ISSUED + }); + + expect(mockCertificateRequestDAL.findById).toHaveBeenCalledWith("550e8400-e29b-41d4-a716-446655440011"); + expect(mockCertificateRequestDAL.updateStatus).toHaveBeenCalledWith( + "550e8400-e29b-41d4-a716-446655440011", + CertificateRequestStatus.ISSUED, + undefined + ); + expect(result).toEqual(mockUpdatedRequest); + }); + + it("should update certificate request status with error message", async () => { + const mockRequest = { + id: "550e8400-e29b-41d4-a716-446655440012", + status: CertificateRequestStatus.PENDING + }; + const mockUpdatedRequest = { + id: "550e8400-e29b-41d4-a716-446655440012", + status: CertificateRequestStatus.FAILED + }; + + (mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest); + (mockCertificateRequestDAL.updateStatus as any).mockResolvedValue(mockUpdatedRequest); + + const result = await service.updateCertificateRequestStatus({ + certificateRequestId: "550e8400-e29b-41d4-a716-446655440012", + status: CertificateRequestStatus.FAILED, + errorMessage: "Certificate issuance failed" + }); + + expect(mockCertificateRequestDAL.updateStatus).toHaveBeenCalledWith( + "550e8400-e29b-41d4-a716-446655440012", + CertificateRequestStatus.FAILED, + "Certificate issuance failed" + ); + expect(result).toEqual(mockUpdatedRequest); + }); + + it("should throw NotFoundError when certificate request does not exist", async () => { + (mockCertificateRequestDAL.findById as any).mockResolvedValue(null); + + await expect( + service.updateCertificateRequestStatus({ + certificateRequestId: "550e8400-e29b-41d4-a716-446655440013", + status: CertificateRequestStatus.ISSUED + }) + ).rejects.toThrow(NotFoundError); + }); + }); + + describe("attachCertificateToRequest", () => { + it("should attach certificate to request successfully", async () => { + const mockRequest = { + id: "550e8400-e29b-41d4-a716-446655440014", + status: CertificateRequestStatus.PENDING + }; + const mockCertificate = { + id: "550e8400-e29b-41d4-a716-446655440015" + }; + const mockUpdatedRequest = { + id: "550e8400-e29b-41d4-a716-446655440014", + status: CertificateRequestStatus.ISSUED, + certificateId: "550e8400-e29b-41d4-a716-446655440015" + }; + + (mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest); + (mockCertificateDAL.findById as any).mockResolvedValue(mockCertificate); + (mockCertificateRequestDAL.attachCertificate as any).mockResolvedValue(mockUpdatedRequest); + + const result = await service.attachCertificateToRequest({ + certificateRequestId: "550e8400-e29b-41d4-a716-446655440014", + certificateId: "550e8400-e29b-41d4-a716-446655440015" + }); + + expect(mockCertificateRequestDAL.findById).toHaveBeenCalledWith("550e8400-e29b-41d4-a716-446655440014"); + expect(mockCertificateDAL.findById).toHaveBeenCalledWith("550e8400-e29b-41d4-a716-446655440015"); + expect(mockCertificateRequestDAL.attachCertificate).toHaveBeenCalledWith( + "550e8400-e29b-41d4-a716-446655440014", + "550e8400-e29b-41d4-a716-446655440015" + ); + expect(result).toEqual(mockUpdatedRequest); + }); + + it("should throw NotFoundError when certificate request does not exist", async () => { + (mockCertificateRequestDAL.findById as any).mockResolvedValue(null); + + await expect( + service.attachCertificateToRequest({ + certificateRequestId: "550e8400-e29b-41d4-a716-446655440016", + certificateId: "550e8400-e29b-41d4-a716-446655440017" + }) + ).rejects.toThrow(NotFoundError); + }); + + it("should throw NotFoundError when certificate does not exist", async () => { + const mockRequest = { + id: "550e8400-e29b-41d4-a716-446655440018", + status: CertificateRequestStatus.PENDING + }; + + (mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest); + (mockCertificateDAL.findById as any).mockResolvedValue(null); + + await expect( + service.attachCertificateToRequest({ + certificateRequestId: "550e8400-e29b-41d4-a716-446655440018", + certificateId: "550e8400-e29b-41d4-a716-446655440019" + }) + ).rejects.toThrow(NotFoundError); + }); + }); +}); diff --git a/backend/src/services/certificate-request/certificate-request-service.ts b/backend/src/services/certificate-request/certificate-request-service.ts new file mode 100644 index 000000000..15450ee6e --- /dev/null +++ b/backend/src/services/certificate-request/certificate-request-service.ts @@ -0,0 +1,284 @@ +import { ForbiddenError } from "@casl/ability"; +import { Knex } from "knex"; +import { z } from "zod"; + +import { ActionProjectType } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + ProjectPermissionCertificateActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; + +import { ActorType } from "../auth/auth-type"; +import { TCertificateRequestDALFactory } from "./certificate-request-dal"; +import { + CertificateRequestStatus, + TAttachCertificateToRequestDTO, + TCreateCertificateRequestDTO, + TGetCertificateFromRequestDTO, + TGetCertificateRequestDTO, + TUpdateCertificateRequestStatusDTO +} from "./certificate-request-types"; + +type TCertificateRequestServiceFactoryDep = { + certificateRequestDAL: TCertificateRequestDALFactory; + certificateDAL: Pick; + certificateService: Pick; + permissionService: Pick; +}; + +export type TCertificateRequestServiceFactory = ReturnType; + +const certificateRequestDataSchema = z + .object({ + profileId: z.string().uuid().optional(), + caId: z.string().uuid().optional(), + csr: z.string().min(1).optional(), + commonName: z.string().max(255).optional(), + altNames: z.string().max(1000).optional(), + keyUsages: z.array(z.string()).max(20).optional(), + extendedKeyUsages: z.array(z.string()).max(20).optional(), + notBefore: z.date().optional(), + notAfter: z.date().optional(), + keyAlgorithm: z.string().max(100).optional(), + signatureAlgorithm: z.string().max(100).optional(), + metadata: z.string().max(2000).optional(), + certificateId: z.string().optional() + }) + .refine( + (data) => { + // Must have either profileId or caId + return data.profileId || data.caId; + }, + { + message: "Either profileId or caId must be provided" + } + ) + .refine( + (data) => { + // If notAfter is provided, it must be after notBefore + if (data.notBefore && data.notAfter) { + return data.notAfter > data.notBefore; + } + return true; + }, + { + message: "notAfter must be after notBefore" + } + ); + +const validateCertificateRequestData = (data: unknown) => { + try { + return certificateRequestDataSchema.parse(data); + } catch (error) { + if (error instanceof z.ZodError) { + throw new BadRequestError({ + message: `Invalid certificate request data: ${error.errors.map((e) => e.message).join(", ")}` + }); + } + throw error; + } +}; + +export const certificateRequestServiceFactory = ({ + certificateRequestDAL, + certificateDAL, + certificateService, + permissionService +}: TCertificateRequestServiceFactoryDep) => { + const createCertificateRequest = async ({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId, + tx, + status, + ...requestData + }: TCreateCertificateRequestDTO & { tx?: Knex }) => { + if (actor !== ActorType.ACME_ACCOUNT) { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Create, + ProjectPermissionSub.Certificates + ); + } + + // Validate input data before creating the request + const validatedData = validateCertificateRequestData(requestData); + + const certificateRequest = await certificateRequestDAL.create( + { + status, + projectId, + ...validatedData + }, + tx + ); + + return certificateRequest; + }; + + const getCertificateRequest = async ({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId, + certificateRequestId + }: TGetCertificateRequestDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); + + const certificateRequest = await certificateRequestDAL.findById(certificateRequestId); + if (!certificateRequest) { + throw new NotFoundError({ message: "Certificate request not found" }); + } + + if (certificateRequest.projectId !== projectId) { + throw new NotFoundError({ message: "Certificate request not found" }); + } + + return certificateRequest; + }; + + const getCertificateFromRequest = async ({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId, + certificateRequestId + }: TGetCertificateFromRequestDTO) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionCertificateActions.Read, + ProjectPermissionSub.Certificates + ); + + const certificateRequest = await certificateRequestDAL.findByIdWithCertificate(certificateRequestId); + if (!certificateRequest) { + throw new NotFoundError({ message: "Certificate request not found" }); + } + + if (certificateRequest.projectId !== projectId) { + throw new NotFoundError({ message: "Certificate request not found" }); + } + + // If no certificate is attached, return basic info + if (!certificateRequest.certificate) { + return { + status: certificateRequest.status as CertificateRequestStatus, + certificate: null, + privateKey: null, + serialNumber: null, + errorMessage: certificateRequest.errorMessage || null, + createdAt: certificateRequest.createdAt, + updatedAt: certificateRequest.updatedAt + }; + } + + // Get certificate body (PEM data) + const certBody = await certificateService.getCertBody({ + id: certificateRequest.certificate.id, + actor, + actorId, + actorAuthMethod, + actorOrgId + }); + + // Try to get private key (may fail if user doesn't have permission) + let privateKey: string | null = null; + try { + const certPrivateKey = await certificateService.getCertPrivateKey({ + id: certificateRequest.certificate.id, + actor, + actorId, + actorAuthMethod, + actorOrgId + }); + privateKey = certPrivateKey.certPrivateKey; + } catch (error) { + // Private key access denied - continue without it + privateKey = null; + } + + return { + status: certificateRequest.status as CertificateRequestStatus, + certificate: certBody.certificate, + privateKey, + serialNumber: certificateRequest.certificate.serialNumber, + errorMessage: certificateRequest.errorMessage || null, + createdAt: certificateRequest.createdAt, + updatedAt: certificateRequest.updatedAt + }; + }; + + const updateCertificateRequestStatus = async ({ + certificateRequestId, + status, + errorMessage + }: TUpdateCertificateRequestStatusDTO) => { + const certificateRequest = await certificateRequestDAL.findById(certificateRequestId); + if (!certificateRequest) { + throw new NotFoundError({ message: "Certificate request not found" }); + } + + return certificateRequestDAL.updateStatus(certificateRequestId, status, errorMessage); + }; + + const attachCertificateToRequest = async ({ + certificateRequestId, + certificateId + }: TAttachCertificateToRequestDTO) => { + const certificateRequest = await certificateRequestDAL.findById(certificateRequestId); + if (!certificateRequest) { + throw new NotFoundError({ message: "Certificate request not found" }); + } + + const certificate = await certificateDAL.findById(certificateId); + if (!certificate) { + throw new NotFoundError({ message: "Certificate not found" }); + } + + return certificateRequestDAL.attachCertificate(certificateRequestId, certificateId); + }; + + return { + createCertificateRequest, + getCertificateRequest, + getCertificateFromRequest, + updateCertificateRequestStatus, + attachCertificateToRequest + }; +}; diff --git a/backend/src/services/certificate-request/certificate-request-types.ts b/backend/src/services/certificate-request/certificate-request-types.ts new file mode 100644 index 000000000..c8a00de7e --- /dev/null +++ b/backend/src/services/certificate-request/certificate-request-types.ts @@ -0,0 +1,43 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum CertificateRequestStatus { + PENDING = "pending", + ISSUED = "issued", + FAILED = "failed" +} + +export type TCreateCertificateRequestDTO = TProjectPermission & { + profileId?: string; + caId?: string; + csr?: string; + commonName?: string; + altNames?: string; + keyUsages?: string[]; + extendedKeyUsages?: string[]; + notBefore?: Date; + notAfter?: Date; + keyAlgorithm?: string; + signatureAlgorithm?: string; + metadata?: string; + status: CertificateRequestStatus; + certificateId?: string; +}; + +export type TGetCertificateRequestDTO = TProjectPermission & { + certificateRequestId: string; +}; + +export type TGetCertificateFromRequestDTO = TProjectPermission & { + certificateRequestId: string; +}; + +export type TUpdateCertificateRequestStatusDTO = { + certificateRequestId: string; + status: CertificateRequestStatus; + errorMessage?: string; +}; + +export type TAttachCertificateToRequestDTO = { + certificateRequestId: string; + certificateId: string; +}; diff --git a/backend/src/services/certificate-v3/certificate-v3-fns.ts b/backend/src/services/certificate-v3/certificate-v3-fns.ts new file mode 100644 index 000000000..58a3a5d20 --- /dev/null +++ b/backend/src/services/certificate-v3/certificate-v3-fns.ts @@ -0,0 +1,40 @@ +import RE2 from "re2"; + +import { BadRequestError } from "@app/lib/errors"; + +export const parseTtlToDays = (ttl: string): number => { + const match = ttl.match(new RE2("^(\\d+)([dhm])$")); + if (!match) { + throw new BadRequestError({ message: `Invalid TTL format: ${ttl}` }); + } + + const [, value, unit] = match; + const num = parseInt(value, 10); + + switch (unit) { + case "d": + return num; + case "h": + return Math.ceil(num / 24); + case "m": + return Math.ceil(num / (24 * 60)); + default: + throw new BadRequestError({ message: `Invalid TTL unit: ${unit}` }); + } +}; + +export const calculateRenewalThreshold = ( + profileRenewBeforeDays: number | undefined, + certificateTtlInDays: number +): number | undefined => { + if (profileRenewBeforeDays === undefined) { + return undefined; + } + + if (profileRenewBeforeDays >= certificateTtlInDays) { + // If renewBeforeDays >= TTL, renew 1 day before expiry + return Math.max(1, certificateTtlInDays - 1); + } + + return profileRenewBeforeDays; +}; diff --git a/backend/src/services/certificate-v3/certificate-v3-service.test.ts b/backend/src/services/certificate-v3/certificate-v3-service.test.ts index 6c664e324..9d8d1aebb 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.test.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.test.ts @@ -11,7 +11,7 @@ import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-ac import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; -import { ACMESANType, CertificateOrderStatus, CertStatus } from "@app/services/certificate/certificate-types"; +import { CertStatus } from "@app/services/certificate/certificate-types"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums"; import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; @@ -19,10 +19,11 @@ import { CertExtendedKeyUsageType, CertIncludeType, CertKeyUsageType, - CertSubjectAttributeType + CertSubjectAttributeType, + CertSubjectAlternativeNameType } from "@app/services/certificate-common/certificate-constants"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; -import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; +import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types"; import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { ActorType, AuthMethod } from "../auth/auth-type"; @@ -40,26 +41,50 @@ vi.mock("../certificate-common/certificate-csr-utils", () => ({ describe("CertificateV3Service", () => { let service: TCertificateV3ServiceFactory; - const mockCertificateDAL: Pick = { + const mockCertificateDAL: Pick< + TCertificateDALFactory, + "findOne" | "findById" | "updateById" | "transaction" | "create" | "find" + > = { findOne: vi.fn(), findById: vi.fn(), updateById: vi.fn(), + create: vi.fn().mockResolvedValue({ + id: "new-cert-id", + serialNumber: "123456789", + friendlyName: "Test Certificate", + commonName: "test.example.com", + status: "ACTIVE" + }), + transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise) => { + const mockTx = {}; + return callback(mockTx); + }), + find: vi.fn().mockResolvedValue([]) + }; + + const mockCertificateSecretDAL: Pick = { + findOne: vi.fn(), + create: vi.fn() + }; + + const mockCertificateAuthorityDAL: Pick< + TCertificateAuthorityDALFactory, + "findByIdWithAssociatedCa" | "create" | "updateById" | "findById" | "transaction" | "findWithAssociatedCa" + > = { + findByIdWithAssociatedCa: vi.fn(), + create: vi.fn().mockResolvedValue({ id: "ca-123" }), + updateById: vi.fn().mockResolvedValue({ id: "ca-123" }), + findById: vi.fn().mockResolvedValue({ id: "ca-123" }), + findWithAssociatedCa: vi.fn().mockResolvedValue([]), transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise) => { const mockTx = {}; return callback(mockTx); }) }; - const mockCertificateSecretDAL: Pick = { - findOne: vi.fn() - }; - - const mockCertificateAuthorityDAL: Pick = { - findByIdWithAssociatedCa: vi.fn() - }; - - const mockCertificateProfileDAL: Pick = { - findByIdWithConfigs: vi.fn() + const mockCertificateProfileDAL: Pick = { + findByIdWithConfigs: vi.fn(), + findById: vi.fn() }; const mockCertificateTemplateV2Service: Pick< @@ -150,6 +175,33 @@ describe("CertificateV3Service", () => { }, pkiSyncQueue: { queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined) + }, + certificateBodyDAL: { + create: vi.fn().mockResolvedValue({ id: "body-123" }) + }, + kmsService: { + generateKmsKey: vi.fn().mockResolvedValue("kms-key-123"), + encryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("encrypted"))), + decryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("decrypted"))), + createCipherPairWithDataKey: vi.fn().mockResolvedValue({ + cipherTextBlob: Buffer.from("encrypted"), + plainTextKey: Buffer.from("plainkey") + }) + }, + projectDAL: { + findOne: vi.fn().mockResolvedValue({ id: "project-123" }), + findById: vi.fn().mockResolvedValue({ id: "project-123" }), + updateById: vi.fn().mockResolvedValue({ id: "project-123" }), + transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise) => { + const mockTx = {}; + return callback(mockTx); + }) + } as any, + certificateIssuanceQueue: { + queueCertificateIssuance: vi.fn().mockResolvedValue(undefined) + }, + certificateRequestService: { + createCertificateRequest: vi.fn().mockResolvedValue({ id: "cert-req-123" }) } }); }); @@ -175,6 +227,7 @@ describe("CertificateV3Service", () => { id: profileId, projectId: "project-123", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", createdAt: new Date(), @@ -238,6 +291,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "issuing-ca", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: { id: "ca-123", projectId: "project-123", @@ -297,8 +352,13 @@ describe("CertificateV3Service", () => { vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord); vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); + const result = await service.issueCertificateFromProfile({ profileId, certificateRequest: mockCertificateRequest, @@ -319,6 +379,7 @@ describe("CertificateV3Service", () => { id: profileId, projectId: "project-123", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", createdAt: new Date(), @@ -431,6 +492,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "issuing-ca", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: { id: "ca-123", projectId: "project-123", @@ -473,8 +536,34 @@ describe("CertificateV3Service", () => { vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResultWithCa as any); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); + await service.issueCertificateFromProfile({ profileId, certificateRequest: camelCaseRequest, @@ -508,6 +597,7 @@ describe("CertificateV3Service", () => { id: profileId, projectId: "project-123", enrollmentType: EnrollmentType.EST, // Wrong enrollment type + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", createdAt: new Date(), @@ -561,6 +651,7 @@ describe("CertificateV3Service", () => { id: profileId, projectId: "project-123", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", createdAt: new Date(), @@ -697,8 +788,13 @@ describe("CertificateV3Service", () => { }); vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord); vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); + const result = await service.signCertificateFromProfile({ profileId, csr: mockCSR, @@ -721,6 +817,7 @@ describe("CertificateV3Service", () => { id: profileId, projectId: "project-123", enrollmentType: EnrollmentType.EST, // Wrong enrollment type + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", createdAt: new Date(), @@ -757,7 +854,7 @@ describe("CertificateV3Service", () => { describe("orderCertificateFromProfile", () => { const mockCertificateOrder = { - altNames: [{ type: ACMESANType.DNS, value: "example.com" }], + altNames: [{ type: CertSubjectAlternativeNameType.DNS_NAME, value: "example.com" }], validity: { ttl: "30d" }, commonName: "example.com", keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE], @@ -766,173 +863,13 @@ describe("CertificateV3Service", () => { keyAlgorithm: "RSA_2048" }; - it("should create order successfully for API enrollment profile", async () => { - const profileId = "profile-123"; - const mockProfile = { - id: profileId, - projectId: "project-123", - enrollmentType: EnrollmentType.API, - caId: "ca-123", - certificateTemplateId: "template-123", - createdAt: new Date(), - updatedAt: new Date(), - slug: "test-profile-order", - description: "Test order profile", - estConfigId: null, - apiConfigId: null - }; - - const mockCA = { - id: "ca-123", - projectId: "project-123", - externalCa: undefined, - internalCa: { - id: "internal-ca-123", - parentCaId: null, - type: "ROOT", - friendlyName: "Test CA", - organization: "Test Org", - ou: "Test OU", - country: "US", - province: "CA", - locality: "SF", - commonName: "Test CA", - dn: "CN=Test CA", - serialNumber: "123", - maxPathLength: null, - keyAlgorithm: "RSA_2048", - notBefore: undefined, - notAfter: undefined, - activeCaCertId: "cert-123", - caId: "ca-123" - }, - name: "Test CA", - status: "ACTIVE", - createdAt: new Date(), - updatedAt: new Date(), - enableDirectIssuance: true - }; - - const mockTemplate = { - id: "template-123", - name: "Test Order Template", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project-123", - description: "Test template for ordering certificates", - signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" }, - keyAlgorithm: { defaultKeyType: "RSA_2048" }, - attributes: [ - { - type: CertSubjectAttributeType.COMMON_NAME, - include: CertIncludeType.OPTIONAL, - value: ["example.com"] - } - ], - subject: undefined, - sans: undefined, - keyUsages: undefined, - extendedKeyUsages: undefined, - algorithms: undefined, - validity: undefined - }; - - const mockCertificateResult = { - certificate: "cert", - certificateChain: "chain", - issuingCaCertificate: "issuing-ca", - privateKey: "key", - serialNumber: "123456", - ca: { - id: "ca-123", - projectId: "project-123", - name: "Test CA", - status: "ACTIVE", - createdAt: new Date(), - updatedAt: new Date(), - enableDirectIssuance: true, - externalCa: undefined, - internalCa: { - id: "internal-ca-123", - parentCaId: null, - type: "ROOT", - friendlyName: "Test CA", - organization: "Test Org", - ou: "Test OU", - country: "US", - province: "CA", - locality: "SF", - commonName: "Test CA", - dn: "CN=Test CA", - serialNumber: "123", - maxPathLength: null, - keyAlgorithm: "RSA_2048", - notBefore: null, - notAfter: null, - activeCaCertId: "cert-123", - caId: "ca-123" - } - } - }; - - const mockCertRecord = { - id: "cert-123", - serialNumber: "123456", - status: "ACTIVE", - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project-123", - commonName: "example.com", - friendlyName: "Test Order Cert", - notBefore: new Date(), - notAfter: new Date(), - caId: "ca-123", - certificateTemplateId: "template-123", - revokedAt: null, - altNames: JSON.stringify([{ type: "DNS", value: "example.com" }]), - caCertId: null, - keyUsages: ["DIGITAL_SIGNATURE"], - extendedKeyUsages: ["SERVER_AUTH"], - revocationReason: null, - pkiSubscriberId: null, - profileId: null - }; - - vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); - vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({ - isValid: true, - errors: [], - warnings: [] - }); - vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); - vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate); - vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any); - vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord); - vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord); - - const result = await service.orderCertificateFromProfile({ - profileId, - certificateOrder: mockCertificateOrder, - ...mockActor - }); - - expect(result).toHaveProperty("orderId"); - expect(result).toHaveProperty("status", "valid"); - expect(result).toHaveProperty("certificate"); - expect(result.subjectAlternativeNames).toHaveLength(1); - expect(result.subjectAlternativeNames[0]).toEqual({ - type: ACMESANType.DNS, - value: "example.com", - status: CertificateOrderStatus.VALID - }); - }); - it("should throw ForbiddenRequestError when profile is not configured for API enrollment", async () => { const profileId = "profile-123"; const mockProfile = { id: profileId, projectId: "project-123", enrollmentType: EnrollmentType.EST, // Wrong enrollment type + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", createdAt: new Date(), @@ -971,6 +908,7 @@ describe("CertificateV3Service", () => { caId: "ca-1", certificateTemplateId: "template-1", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, createdAt: new Date(), updatedAt: new Date(), description: "Test profile for algorithm compatibility", @@ -1061,6 +999,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "ca-cert", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: rsaCa as any }); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ @@ -1107,6 +1047,31 @@ describe("CertificateV3Service", () => { pkiSubscriberId: null, profileId: null }); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); // Should not throw - RSA CA is compatible with RSA signature algorithms await expect( @@ -1193,6 +1158,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "ca-cert", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: ecCa as any }); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ @@ -1239,6 +1206,31 @@ describe("CertificateV3Service", () => { pkiSubscriberId: null, profileId: null }); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); // Should not throw - EC CA is compatible with ECDSA signature algorithms await expect( @@ -1325,6 +1317,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "ca-cert", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: rsa8192Ca as any }); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ @@ -1371,6 +1365,31 @@ describe("CertificateV3Service", () => { pkiSubscriberId: null, profileId: null }); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); // Should not throw - dynamic check supports new RSA key sizes await expect( @@ -1457,6 +1476,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "ca-cert", privateKey: "key", serialNumber: "123456", + certificateId: "cert-1", + commonName: "test.example.com", ca: newEcCa as any }); vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({ @@ -1503,6 +1524,31 @@ describe("CertificateV3Service", () => { pkiSubscriberId: null, profileId: null }); + vi.mocked(mockCertificateDAL.findById).mockResolvedValue({ + id: "cert-1", + serialNumber: "123456", + status: "ACTIVE", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project-1", + commonName: "test.example.com", + friendlyName: "Test Algorithm Cert", + notBefore: new Date(), + notAfter: new Date(), + caId: "ca-1", + certificateTemplateId: "template-1", + revokedAt: null, + altNames: null, + caCertId: null, + keyUsages: null, + extendedKeyUsages: null, + revocationReason: null, + pkiSubscriberId: null, + profileId: null + }); + vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { + return callback(undefined as any); + }); // Should not throw - dynamic check supports new EC curves await expect( @@ -1552,6 +1598,7 @@ describe("CertificateV3Service", () => { id: "profile-123", projectId: "project-123", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, caId: "ca-123", certificateTemplateId: "template-123", apiConfig: { @@ -1635,8 +1682,9 @@ describe("CertificateV3Service", () => { }); it("should successfully renew eligible certificate", async () => { - // Mock the initial findById call - vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert); + vi.mocked(mockCertificateDAL.findById) + .mockResolvedValueOnce(mockOriginalCert) + .mockResolvedValueOnce({ ...mockOriginalCert, id: "cert-456", serialNumber: "789012" }); vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any); vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile); vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA); @@ -1652,6 +1700,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "issuing-ca", privateKey: "private-key", serialNumber: "789012", + certificateId: "cert-456", + commonName: "test.example.com", ca: mockCA }); @@ -1733,9 +1783,9 @@ describe("CertificateV3Service", () => { }); }); - it("should reject renewal if certificate is not from a profile", async () => { - const certWithoutProfile = { ...mockOriginalCert, profileId: null }; - vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfile); + it("should reject renewal if certificate has no profile and no CA", async () => { + const certWithoutProfileAndCA = { ...mockOriginalCert, profileId: null, caId: null }; + vi.mocked(mockCertificateDAL.findById).mockResolvedValue(certWithoutProfileAndCA); // Set up transaction mock to properly handle errors vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise) => { @@ -1969,6 +2019,8 @@ describe("CertificateV3Service", () => { issuingCaCertificate: "issuing-ca", privateKey: "private-key", serialNumber: "789012", + certificateId: "cert-456", + commonName: "test.example.com", ca: mockCA }); @@ -2008,6 +2060,7 @@ describe("CertificateV3Service", () => { const mockProfile = { id: "profile-123", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, projectId: "project-123" }; @@ -2084,6 +2137,7 @@ describe("CertificateV3Service", () => { const mockProfile = { id: "profile-123", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, projectId: "project-123" }; @@ -2129,6 +2183,7 @@ describe("CertificateV3Service", () => { const mockProfile = { id: "profile-123", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, projectId: "project-123" }; @@ -2172,6 +2227,7 @@ describe("CertificateV3Service", () => { const mockProfile = { id: "profile-123", enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, projectId: "project-123" }; diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index a537ddc06..20e5d3e1c 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -1,8 +1,9 @@ import { ForbiddenError } from "@casl/ability"; +import * as x509 from "@peculiar/x509"; import { randomUUID } from "crypto"; import RE2 from "re2"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, TCertificates } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionCertificateActions, @@ -10,16 +11,16 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-account-dal"; +import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; +import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { - CertExtendedKeyUsage, - CertificateOrderStatus, CertKeyAlgorithm, CertKeyType, - CertKeyUsage, CertSignatureAlgorithm, CertStatus } from "@app/services/certificate/certificate-types"; @@ -28,12 +29,25 @@ import { TCertificateAuthorityWithAssociatedCa } from "@app/services/certificate-authority/certificate-authority-dal"; import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { + createDistinguishedName, + createSerialNumber, + keyAlgorithmToAlgCfg, + signatureAlgorithmToAlgCfg +} from "@app/services/certificate-authority/certificate-authority-fns"; import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; -import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; +import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types"; import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; -import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants"; +import { + CertExtendedKeyUsageType, + CertKeyUsageType, + CertSubjectAlternativeNameType +} from "../certificate-common/certificate-constants"; import { extractAlgorithmsFromCSR, extractCertificateRequestFromCSR @@ -42,15 +56,16 @@ import { bufferToString, buildCertificateSubjectFromTemplate, buildSubjectAlternativeNamesFromTemplate, - convertExtendedKeyUsageArrayFromLegacy, convertExtendedKeyUsageArrayToLegacy, - convertKeyUsageArrayFromLegacy, convertKeyUsageArrayToLegacy, mapEnumsForValidation, normalizeDateForApi, removeRootCaFromChain } from "../certificate-common/certificate-utils"; +import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service"; +import { CertificateRequestStatus } from "../certificate-request/certificate-request-types"; import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal"; +import { TCertificateRequest } from "../certificate-template-v2/certificate-template-v2-types"; import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal"; import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue"; import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils"; @@ -68,10 +83,17 @@ import { } from "./certificate-v3-types"; type TCertificateV3ServiceFactoryDep = { - certificateDAL: Pick; - certificateSecretDAL: Pick; - certificateAuthorityDAL: Pick; - certificateProfileDAL: Pick; + certificateDAL: Pick< + TCertificateDALFactory, + "findOne" | "findById" | "updateById" | "transaction" | "create" | "find" + >; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + certificateAuthorityDAL: Pick< + TCertificateAuthorityDALFactory, + "findByIdWithAssociatedCa" | "create" | "transaction" | "updateById" | "findWithAssociatedCa" | "findById" + >; + certificateProfileDAL: Pick; acmeAccountDAL: Pick; certificateTemplateV2Service: Pick< TCertificateTemplateV2ServiceFactory, @@ -85,6 +107,16 @@ type TCertificateV3ServiceFactoryDep = { >; pkiSyncDAL: Pick; pkiSyncQueue: Pick; + kmsService: Pick< + TKmsServiceFactory, + "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "createCipherPairWithDataKey" + >; + projectDAL: TProjectDALFactory; + certificateIssuanceQueue: Pick< + import("../certificate-authority/certificate-issuance-queue").TCertificateIssuanceQueueFactory, + "queueCertificateIssuance" + >; + certificateRequestService: Pick; }; export type TCertificateV3ServiceFactory = ReturnType; @@ -272,16 +304,62 @@ const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | s return bufferToString(certData as unknown as Buffer); }; -const parseKeyUsages = (keyUsages: unknown): CertKeyUsage[] => { +const parseKeyUsages = (keyUsages: unknown): CertKeyUsageType[] => { if (!keyUsages) return []; - if (Array.isArray(keyUsages)) return keyUsages as CertKeyUsage[]; - return (keyUsages as string).split(",").map((usage) => usage.trim() as CertKeyUsage); + + const validKeyUsages = Object.values(CertKeyUsageType); + + if (Array.isArray(keyUsages)) { + return keyUsages.filter( + (usage): usage is CertKeyUsageType => + typeof usage === "string" && validKeyUsages.includes(usage as CertKeyUsageType) + ); + } + + if (typeof keyUsages === "string") { + return keyUsages + .split(",") + .map((usage) => usage.trim()) + .filter((usage): usage is CertKeyUsageType => validKeyUsages.includes(usage as CertKeyUsageType)); + } + + return []; }; -const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsage[] => { +const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsageType[] => { if (!extendedKeyUsages) return []; - if (Array.isArray(extendedKeyUsages)) return extendedKeyUsages as CertExtendedKeyUsage[]; - return (extendedKeyUsages as string).split(",").map((usage) => usage.trim() as CertExtendedKeyUsage); + + const validExtendedKeyUsages = Object.values(CertExtendedKeyUsageType); + + if (Array.isArray(extendedKeyUsages)) { + return extendedKeyUsages.filter( + (usage): usage is CertExtendedKeyUsageType => + typeof usage === "string" && validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType) + ); + } + + if (typeof extendedKeyUsages === "string") { + return extendedKeyUsages + .split(",") + .map((usage) => usage.trim()) + .filter((usage): usage is CertExtendedKeyUsageType => + validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType) + ); + } + + return []; +}; + +const convertEnumsToStringArray = (enumArray: T[]): string[] => { + return enumArray.map((item) => item as string); +}; + +const combineKeyUsageFlags = (keyUsages: string[]): number => { + return keyUsages.reduce((acc: number, usage) => { + const flag = x509.KeyUsageFlags[usage as keyof typeof x509.KeyUsageFlags]; + // eslint-disable-next-line no-bitwise + return typeof flag === "number" ? acc | flag : acc; + }, 0); }; const isValidRenewalTiming = (renewBeforeDays: number, certificateExpiryDate: Date): boolean => { @@ -329,6 +407,154 @@ const parseTtlToDays = (ttl: string): number => { } }; +const generateSelfSignedCertificate = async ({ + certificateRequest, + template, + effectiveSignatureAlgorithm, + effectiveKeyAlgorithm +}: { + certificateRequest: { + commonName?: string; + keyUsages?: CertKeyUsageType[]; + extendedKeyUsages?: CertExtendedKeyUsageType[]; + altNames?: Array<{ + type: CertSubjectAlternativeNameType; + value: string; + }>; + validity: { ttl: string }; + notBefore?: Date; + notAfter?: Date; + }; + template?: { + subject?: Array<{ + type: string; + allowed?: string[]; + required?: string[]; + denied?: string[]; + }>; + sans?: Array<{ + type: string; + allowed?: string[]; + required?: string[]; + denied?: string[]; + }>; + } | null; + effectiveSignatureAlgorithm: CertSignatureAlgorithm; + effectiveKeyAlgorithm: CertKeyAlgorithm; +}): Promise<{ + certificate: Buffer; + privateKey: Buffer; + serialNumber: string; + notBefore: Date; + notAfter: Date; + certificateSubject: Record; + subjectAlternativeNames: Array<{ + type: CertSubjectAlternativeNameType; + value: string; + }>; +}> => { + const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template?.subject); + const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate( + { subjectAlternativeNames: certificateRequest.altNames }, + template?.sans + ); + + const keyGenAlg = keyAlgorithmToAlgCfg(effectiveKeyAlgorithm); + const keyPair = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]); + + const signatureAlgorithmConfig = signatureAlgorithmToAlgCfg(effectiveSignatureAlgorithm, effectiveKeyAlgorithm); + + const notBeforeDate = certificateRequest.notBefore ? new Date(certificateRequest.notBefore) : new Date(); + + let notAfterDate: Date; + if (certificateRequest.notAfter) { + notAfterDate = new Date(certificateRequest.notAfter); + } else if (certificateRequest.validity.ttl) { + notAfterDate = new Date(new Date().getTime() + ms(certificateRequest.validity.ttl)); + } else { + throw new BadRequestError({ + message: "Either notAfter date or TTL must be provided for certificate validity" + }); + } + + const serialNumber = createSerialNumber(); + const dn = createDistinguishedName({ + commonName: certificateSubject.common_name, + organization: certificateSubject.organization, + ou: certificateSubject.organizational_unit, + country: certificateSubject.country, + province: certificateSubject.state_or_province_name, + locality: certificateSubject.locality_name + }); + + const cert = await x509.X509CertificateGenerator.createSelfSigned({ + name: dn, + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingAlgorithm: signatureAlgorithmConfig, + keys: keyPair, + extensions: [ + new x509.BasicConstraintsExtension(false, undefined, false), + ...(certificateRequest.keyUsages?.length + ? [ + new x509.KeyUsagesExtension( + combineKeyUsageFlags(convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || []), + false + ) + ] + : []), + ...(certificateRequest.extendedKeyUsages?.length + ? [ + new x509.ExtendedKeyUsageExtension( + (convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || []).map( + (eku) => x509.ExtendedKeyUsage[eku] + ), + false + ) + ] + : []), + ...(subjectAlternativeNames + ? [ + new x509.SubjectAlternativeNameExtension( + certificateRequest.altNames?.map((san) => { + switch (san.type) { + case CertSubjectAlternativeNameType.DNS_NAME: + return { type: "dns" as const, value: san.value }; + case CertSubjectAlternativeNameType.IP_ADDRESS: + return { type: "ip" as const, value: san.value }; + case CertSubjectAlternativeNameType.EMAIL: + return { type: "email" as const, value: san.value }; + case CertSubjectAlternativeNameType.URI: + return { type: "url" as const, value: san.value }; + default: + throw new BadRequestError({ + message: `Unsupported Subject Alternative Name type: ${san.type as string}` + }); + } + }) || [], + false + ) + ] + : []) + ] + }); + + const certificatePem = cert.toString("pem"); + const privateKeyObj = crypto.nativeCrypto.KeyObject.from(keyPair.privateKey); + const privateKeyPem = privateKeyObj.export({ format: "pem", type: "pkcs8" }) as string; + + return { + certificate: Buffer.from(certificatePem), + privateKey: Buffer.from(privateKeyPem), + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate, + certificateSubject, + subjectAlternativeNames: certificateRequest.altNames || [] + }; +}; + const calculateFinalRenewBeforeDays = ( profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } }, ttl: string, @@ -348,8 +574,279 @@ const calculateFinalRenewBeforeDays = ( return isValidRenewalTiming(renewBeforeDays, certificateExpiryDate) ? renewBeforeDays : undefined; }; +const getEffectiveAlgorithms = ( + requestSignatureAlgorithm?: CertSignatureAlgorithm, + requestKeyAlgorithm?: CertKeyAlgorithm, + originalSignatureAlgorithm?: CertSignatureAlgorithm, + originalKeyAlgorithm?: CertKeyAlgorithm +) => { + return { + signatureAlgorithm: requestSignatureAlgorithm || originalSignatureAlgorithm || CertSignatureAlgorithm.RSA_SHA256, + keyAlgorithm: requestKeyAlgorithm || originalKeyAlgorithm || CertKeyAlgorithm.RSA_2048 + }; +}; + +const createSelfSignedCertificateRecord = async ({ + selfSignedResult, + certificateRequest, + profile, + originalCert, + certificateDAL, + tx, + isRenewal = false +}: { + selfSignedResult: Awaited>; + certificateRequest: { + commonName?: string; + keyUsages?: CertKeyUsageType[]; + extendedKeyUsages?: CertExtendedKeyUsageType[]; + }; + profile?: { id: string; projectId: string } | null; + originalCert?: { + id: string; + friendlyName?: string | null; + commonName?: string | null; + projectId: string; + }; + certificateDAL: Pick; + tx: Parameters[1]; + isRenewal?: boolean; +}) => { + const subjectCommonName = + (selfSignedResult.certificateSubject.common_name as string) || + certificateRequest.commonName || + originalCert?.commonName || + ""; + + const altNamesList = selfSignedResult.subjectAlternativeNames.map((san) => san.value).join(","); + + const projectId = originalCert?.projectId || profile?.projectId; + if (!projectId) { + throw new BadRequestError({ message: "Project ID is required for certificate creation" }); + } + + const baseRecord = { + serialNumber: selfSignedResult.serialNumber, + friendlyName: originalCert?.friendlyName || subjectCommonName, + commonName: subjectCommonName, + altNames: altNamesList, + status: CertStatus.ACTIVE, + notBefore: selfSignedResult.notBefore, + notAfter: selfSignedResult.notAfter, + projectId, + keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || [], + extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || [], + profileId: profile?.id || null + }; + + const renewalRecord = + isRenewal && originalCert + ? { + renewedFromCertificateId: originalCert.id + } + : {}; + + return certificateDAL.create( + { + ...baseRecord, + ...renewalRecord + }, + tx + ); +}; + +const createEncryptedCertificateData = async ({ + certificateId, + certificate, + privateKey, + projectId, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + projectDAL, + tx +}: { + certificateId: string; + certificate: Buffer; + privateKey: Buffer; + projectId: string; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + kmsService: Pick; + projectDAL: TProjectDALFactory; + tx: Parameters[1]; +}) => { + const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ + projectId, + projectDAL, + kmsService + }); + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ kmsId: certificateManagerKeyId }); + + const encryptedCertificate = await kmsEncryptor({ + plainText: certificate + }); + + await certificateBodyDAL.create( + { + certId: certificateId, + encryptedCertificate: encryptedCertificate.cipherTextBlob + }, + tx + ); + + const encryptedPrivateKey = await kmsEncryptor({ + plainText: privateKey + }); + + await certificateSecretDAL.create( + { + certId: certificateId, + encryptedPrivateKey: encryptedPrivateKey.cipherTextBlob + }, + tx + ); +}; + +const processSelfSignedCertificate = async ({ + certificateRequest, + template, + profile, + originalCert, + effectiveAlgorithms, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + projectDAL, + tx, + isRenewal = false +}: { + certificateRequest: { + commonName?: string; + keyUsages?: CertKeyUsageType[]; + extendedKeyUsages?: CertExtendedKeyUsageType[]; + validity: { ttl: string }; + notBefore?: Date; + notAfter?: Date; + }; + template?: { + subject?: Array<{ + type: string; + allowed?: string[]; + required?: string[]; + denied?: string[]; + }>; + sans?: Array<{ + type: string; + allowed?: string[]; + required?: string[]; + denied?: string[]; + }>; + } | null; + profile?: { id: string; projectId: string } | null; + originalCert?: { + id: string; + friendlyName?: string | null; + commonName?: string | null; + projectId: string; + }; + effectiveAlgorithms: { + signatureAlgorithm: CertSignatureAlgorithm; + keyAlgorithm: CertKeyAlgorithm; + }; + certificateDAL: Pick; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + kmsService: Pick; + projectDAL: TProjectDALFactory; + tx: Parameters[1]; + isRenewal?: boolean; +}) => { + const projectId = originalCert?.projectId || profile?.projectId; + if (!projectId) { + throw new BadRequestError({ message: "Project ID is required for certificate creation" }); + } + + const selfSignedResult = await generateSelfSignedCertificate({ + certificateRequest, + template, + effectiveSignatureAlgorithm: effectiveAlgorithms.signatureAlgorithm, + effectiveKeyAlgorithm: effectiveAlgorithms.keyAlgorithm + }); + + const certificateData = await createSelfSignedCertificateRecord({ + selfSignedResult, + certificateRequest, + profile, + originalCert, + certificateDAL, + tx, + isRenewal + }); + + await certificateDAL.updateById( + certificateData.id, + { + signatureAlgorithm: effectiveAlgorithms.signatureAlgorithm, + keyAlgorithm: effectiveAlgorithms.keyAlgorithm + }, + tx + ); + + await createEncryptedCertificateData({ + certificateId: certificateData.id, + certificate: selfSignedResult.certificate, + privateKey: selfSignedResult.privateKey, + projectId, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + projectDAL, + tx + }); + + return { + selfSignedResult, + certificateData + }; +}; + +const detectSanType = (value: string): { type: CertSubjectAlternativeNameType; value: string } => { + const isIpv4 = new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(value); + const isIpv6 = new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(value); + + if (isIpv4 || isIpv6) { + return { + type: CertSubjectAlternativeNameType.IP_ADDRESS, + value + }; + } + + if (new RE2("^[^@]+@[^@]+\\.[^@]+$").test(value)) { + return { + type: CertSubjectAlternativeNameType.EMAIL, + value + }; + } + + if (new RE2("^[a-zA-Z][a-zA-Z0-9+.-]*:").test(value)) { + return { + type: CertSubjectAlternativeNameType.URI, + value + }; + } + + return { + type: CertSubjectAlternativeNameType.DNS_NAME, + value + }; +}; + export const certificateV3ServiceFactory = ({ certificateDAL, + certificateBodyDAL, certificateSecretDAL, certificateAuthorityDAL, certificateProfileDAL, @@ -359,7 +856,11 @@ export const certificateV3ServiceFactory = ({ permissionService, certificateSyncDAL, pkiSyncDAL, - pkiSyncQueue + pkiSyncQueue, + kmsService, + projectDAL, + certificateIssuanceQueue, + certificateRequestService }: TCertificateV3ServiceFactoryDep) => { const issueCertificateFromProfile = async ({ profileId, @@ -416,15 +917,6 @@ export const certificateV3ServiceFactory = ({ }); } - const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); - if (!ca) { - throw new NotFoundError({ message: "Certificate Authority not found" }); - } - - validateCaSupport(ca, "direct certificate issuance"); - - validateAlgorithmCompatibility(ca, template); - const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined; const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm as CertKeyAlgorithm | undefined; @@ -440,14 +932,109 @@ export const certificateV3ServiceFactory = ({ }); } - const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.subject); + const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template?.subject); const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate( { subjectAlternativeNames: certificateRequest.altNames }, - template.sans + template?.sans ); - const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } = - await internalCaService.issueCertFromCa({ + const issuerType = profile?.issuerType || (profile?.caId ? IssuerType.CA : IssuerType.SELF_SIGNED); + + if (issuerType === IssuerType.SELF_SIGNED) { + const result = await certificateDAL.transaction(async (tx) => { + const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm); + + const selfSignedResult = await processSelfSignedCertificate({ + certificateRequest, + template, + profile, + effectiveAlgorithms, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + projectDAL, + tx + }); + + const certRequestResult = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: profile.projectId, + tx, + profileId: profile.id, + commonName: certificateRequest.commonName, + altNames: certificateRequest.altNames?.map((san) => san.value).join(","), + keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages), + extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages), + notBefore: certificateRequest.notBefore, + notAfter: certificateRequest.notAfter, + keyAlgorithm: effectiveKeyAlgorithm, + signatureAlgorithm: effectiveSignatureAlgorithm, + status: CertificateRequestStatus.ISSUED, + certificateId: selfSignedResult.certificateData.id + }); + + return { ...selfSignedResult, certificateRequestId: certRequestResult.id }; + }); + + const { selfSignedResult, certificateData, certificateRequestId } = result; + + const subjectCommonName = + (selfSignedResult.certificateSubject.common_name as string) || + certificateRequest.commonName || + "Self-signed Certificate"; + + const finalRenewBeforeDays = calculateFinalRenewBeforeDays( + profile, + certificateRequest.validity.ttl, + selfSignedResult.notAfter + ); + + if (finalRenewBeforeDays !== undefined) { + await certificateDAL.updateById(certificateData.id, { + renewBeforeDays: finalRenewBeforeDays + }); + } + + return { + certificate: selfSignedResult.certificate.toString("utf8"), + issuingCaCertificate: "", + certificateChain: selfSignedResult.certificate.toString("utf8"), + privateKey: selfSignedResult.privateKey.toString("utf8"), + serialNumber: selfSignedResult.serialNumber, + certificateId: certificateData.id, + certificateRequestId, + projectId: profile.projectId, + profileName: profile.slug, + commonName: subjectCommonName + }; + } + + if (!profile.caId) { + throw new NotFoundError({ message: "Certificate Authority ID not found" }); + } + + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); + if (!ca) { + throw new NotFoundError({ message: "Certificate Authority not found" }); + } + + validateCaSupport(ca, "direct certificate issuance"); + validateAlgorithmCompatibility(ca, template); + + const { + certificate, + certificateChain, + issuingCaCertificate, + privateKey, + serialNumber, + cert, + certificateRequestId + } = await certificateDAL.transaction(async (tx) => { + const certResult = await internalCaService.issueCertFromCa({ caId: ca.id, friendlyName: certificateSubject.common_name || "Certificate", commonName: certificateSubject.common_name || "", @@ -463,23 +1050,49 @@ export const certificateV3ServiceFactory = ({ actorId, actorAuthMethod, actorOrgId, - isFromProfile: true + isFromProfile: true, + tx }); - const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id }); - if (!cert) { - throw new NotFoundError({ message: "Certificate was issued but could not be found in database" }); - } + const certificateRecord = await certificateDAL.findById(certResult.certificateId, tx); + if (!certificateRecord) { + throw new NotFoundError({ message: "Certificate was issued but could not be found in database" }); + } - const finalRenewBeforeDays = calculateFinalRenewBeforeDays( - profile, - certificateRequest.validity.ttl, - new Date(cert.notAfter) - ); + const finalRenewBeforeDays = calculateFinalRenewBeforeDays( + profile, + certificateRequest.validity.ttl, + new Date(certificateRecord.notAfter) + ); - await certificateDAL.updateById(cert.id, { - profileId, - renewBeforeDays: finalRenewBeforeDays + const updateData: { profileId: string; renewBeforeDays?: number } = { profileId }; + if (finalRenewBeforeDays !== undefined) { + updateData.renewBeforeDays = finalRenewBeforeDays; + } + await certificateDAL.updateById(certificateRecord.id, updateData, tx); + + const certRequestResult = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: profile.projectId, + tx, + caId: ca.id, + profileId: profile.id, + commonName: certificateRequest.commonName, + altNames: certificateRequest.altNames?.map((san) => san.value).join(","), + keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages), + extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages), + notBefore: certificateRequest.notBefore, + notAfter: certificateRequest.notAfter, + keyAlgorithm: effectiveKeyAlgorithm, + signatureAlgorithm: effectiveSignatureAlgorithm, + status: CertificateRequestStatus.ISSUED, + certificateId: certResult.certificateId + }); + + return { ...certResult, cert: certificateRecord, certificateRequestId: certRequestResult.id }; }); let finalCertificateChain = bufferToString(certificateChain); @@ -494,6 +1107,7 @@ export const certificateV3ServiceFactory = ({ privateKey: bufferToString(privateKey), serialNumber, certificateId: cert.id, + certificateRequestId, projectId: profile.projectId, profileName: profile.slug, commonName: cert.commonName || "" @@ -525,6 +1139,12 @@ export const certificateV3ServiceFactory = ({ enrollmentType ); + if (!profile.caId) { + throw new BadRequestError({ + message: "Self-signed certificates are not supported for CSR signing" + }); + } + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); if (!ca) { throw new NotFoundError({ message: "Certificate Authority not found" }); @@ -571,32 +1191,64 @@ export const certificateV3ServiceFactory = ({ const effectiveSignatureAlgorithm = extractedSignatureAlgorithm; const effectiveKeyAlgorithm = extractedKeyAlgorithm; - const { certificate, certificateChain, issuingCaCertificate, serialNumber } = - await internalCaService.signCertFromCa({ - isInternal: true, - caId: ca.id, - csr, - ttl: validity.ttl, - altNames: undefined, - notBefore: normalizeDateForApi(notBefore), - notAfter: normalizeDateForApi(notAfter), - signatureAlgorithm: effectiveSignatureAlgorithm, - keyAlgorithm: effectiveKeyAlgorithm, - isFromProfile: true + const { certificate, certificateChain, issuingCaCertificate, serialNumber, cert, certificateRequestId } = + await certificateDAL.transaction(async (tx) => { + const certResult = await internalCaService.signCertFromCa({ + isInternal: true, + caId: ca.id, + csr, + ttl: validity.ttl, + altNames: undefined, + notBefore: normalizeDateForApi(notBefore), + notAfter: normalizeDateForApi(notAfter), + signatureAlgorithm: effectiveSignatureAlgorithm, + keyAlgorithm: effectiveKeyAlgorithm, + isFromProfile: true, + tx + }); + + const signedCertRecord = await certificateDAL.findById(certResult.certificateId, tx); + if (!signedCertRecord) { + throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); + } + + const finalRenewBeforeDays = calculateFinalRenewBeforeDays( + profile, + validity.ttl, + new Date(signedCertRecord.notAfter) + ); + + const updateData: { profileId: string; renewBeforeDays?: number } = { profileId }; + if (finalRenewBeforeDays !== undefined) { + updateData.renewBeforeDays = finalRenewBeforeDays; + } + await certificateDAL.updateById(signedCertRecord.id, updateData, tx); + + const certRequestResult = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: profile.projectId, + tx, + caId: ca.id, + profileId: profile.id, + csr, + commonName: mappedCertificateRequest.commonName, + altNames: mappedCertificateRequest.subjectAlternativeNames?.map((san) => san.value).join(","), + keyUsages: convertKeyUsageArrayToLegacy(mappedCertificateRequest.keyUsages), + extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(mappedCertificateRequest.extendedKeyUsages), + notBefore, + notAfter, + keyAlgorithm: effectiveKeyAlgorithm, + signatureAlgorithm: effectiveSignatureAlgorithm, + status: CertificateRequestStatus.ISSUED, + certificateId: certResult.certificateId + }); + + return { ...certResult, cert: signedCertRecord, certificateRequestId: certRequestResult.id }; }); - const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id }); - if (!cert) { - throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); - } - - const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter)); - - await certificateDAL.updateById(cert.id, { - profileId, - renewBeforeDays: finalRenewBeforeDays - }); - const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer); let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer); if (removeRootsFromChain) { @@ -609,6 +1261,7 @@ export const certificateV3ServiceFactory = ({ certificateChain: certificateChainString, serialNumber, certificateId: cert.id, + certificateRequestId, projectId: profile.projectId, profileName: profile.slug, commonName: cert.commonName || "" @@ -621,8 +1274,7 @@ export const certificateV3ServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId, - removeRootsFromChain + actorOrgId }: TOrderCertificateFromProfileDTO): Promise => { const profile = await validateProfileAndPermissions( profileId, @@ -636,22 +1288,41 @@ export const certificateV3ServiceFactory = ({ EnrollmentType.API ); - const certificateRequest = { - commonName: certificateOrder.commonName, - keyUsages: certificateOrder.keyUsages, - extendedKeyUsages: certificateOrder.extendedKeyUsages, - subjectAlternativeNames: certificateOrder.altNames.map((san) => ({ - type: san.type === "dns" ? CertSubjectAlternativeNameType.DNS_NAME : CertSubjectAlternativeNameType.IP_ADDRESS, - value: san.value - })), - validity: certificateOrder.validity, - notBefore: certificateOrder.notBefore, - notAfter: certificateOrder.notAfter, - signatureAlgorithm: certificateOrder.signatureAlgorithm, - keyAlgorithm: certificateOrder.keyAlgorithm - }; + let certificateRequest: TCertificateRequest; + let extractedKeyAlgorithm: string | undefined; + let extractedSignatureAlgorithm: string | undefined; + + if (certificateOrder.csr) { + certificateRequest = extractCertificateRequestFromCSR(certificateOrder.csr); + const algorithms = extractAlgorithmsFromCSR(certificateOrder.csr); + extractedKeyAlgorithm = algorithms.keyAlgorithm; + extractedSignatureAlgorithm = algorithms.signatureAlgorithm; + certificateRequest.validity = certificateOrder.validity; + if (certificateOrder.notBefore && certificateOrder.notAfter) { + certificateRequest.notBefore = certificateOrder.notBefore; + certificateRequest.notAfter = certificateOrder.notAfter; + } + } else { + certificateRequest = { + commonName: certificateOrder.commonName, + keyUsages: certificateOrder.keyUsages, + extendedKeyUsages: certificateOrder.extendedKeyUsages, + subjectAlternativeNames: certificateOrder.altNames, + validity: certificateOrder.validity, + notBefore: certificateOrder.notBefore, + notAfter: certificateOrder.notAfter, + signatureAlgorithm: certificateOrder.signatureAlgorithm, + keyAlgorithm: certificateOrder.keyAlgorithm + }; + } const mappedCertificateRequest = mapEnumsForValidation(certificateRequest); + + if (certificateOrder.csr) { + mappedCertificateRequest.keyAlgorithm = extractedKeyAlgorithm; + mappedCertificateRequest.signatureAlgorithm = extractedSignatureAlgorithm; + } + const validationResult = await certificateTemplateV2Service.validateCertificateRequest( profile.certificateTemplateId, mappedCertificateRequest @@ -663,6 +1334,12 @@ export const certificateV3ServiceFactory = ({ }); } + if (!profile.caId) { + throw new BadRequestError({ + message: "Self-signed certificates are not supported for certificate ordering" + }); + } + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); if (!ca) { throw new NotFoundError({ message: "Certificate Authority not found" }); @@ -671,42 +1348,61 @@ export const certificateV3ServiceFactory = ({ const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; if (caType === CaType.INTERNAL) { - const certificateResult = await issueCertificateFromProfile({ - profileId, - certificateRequest, + throw new BadRequestError({ + message: "Certificate ordering is not supported for the specified CA type" + }); + } + + if (caType === CaType.ACME || caType === CaType.AZURE_AD_CS) { + const orderId = randomUUID(); + + const certRequest = await certificateRequestService.createCertificateRequest({ actor, actorId, actorAuthMethod, actorOrgId, - removeRootsFromChain + projectId: profile.projectId, + caId: ca.id, + profileId: profile.id, + commonName: certificateOrder.commonName || "", + keyUsages: certificateOrder.keyUsages ? convertEnumsToStringArray(certificateOrder.keyUsages) : [], + extendedKeyUsages: certificateOrder.extendedKeyUsages + ? convertEnumsToStringArray(certificateOrder.extendedKeyUsages) + : [], + keyAlgorithm: certificateOrder.keyAlgorithm || "", + signatureAlgorithm: certificateOrder.signatureAlgorithm || "", + altNames: certificateOrder.altNames?.map((san) => san.value).join(",") || "", + notBefore: certificateOrder.notBefore, + notAfter: certificateOrder.notAfter, + status: CertificateRequestStatus.PENDING }); - const orderId = randomUUID(); + await certificateIssuanceQueue.queueCertificateIssuance({ + certificateId: orderId, + profileId: profile.id, + caId: profile.caId || "", + ttl: certificateOrder.validity?.ttl || "1y", + signatureAlgorithm: certificateOrder.signatureAlgorithm || "", + keyAlgorithm: certificateRequest.keyAlgorithm || "", + commonName: certificateRequest.commonName || "", + altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value) || [], + keyUsages: certificateRequest.keyUsages ? convertEnumsToStringArray(certificateRequest.keyUsages) : [], + extendedKeyUsages: certificateRequest.extendedKeyUsages + ? convertEnumsToStringArray(certificateRequest.extendedKeyUsages) + : [], + certificateRequestId: certRequest.id, + csr: certificateOrder.csr + }); return { - orderId, - status: CertificateOrderStatus.VALID, - subjectAlternativeNames: certificateOrder.altNames.map((san) => ({ - type: san.type, - value: san.value, - status: CertificateOrderStatus.VALID - })), - authorizations: [], - finalize: `/api/v3/pki/certificates/orders/${orderId}/completed`, - certificate: certificateResult.certificate, - projectId: certificateResult.projectId, - profileName: certificateResult.profileName + certificateRequestId: certRequest.id, + projectId: certRequest.projectId, + profileName: profile.slug }; } - if (caType === CaType.ACME) { - throw new BadRequestError({ - message: "ACME certificate ordering via profiles is not yet implemented." - }); - } - throw new BadRequestError({ - message: `Certificate ordering is not supported for CA type: ${caType}` + message: "Certificate ordering is not supported for the specified CA type" }); }; @@ -718,7 +1414,9 @@ export const certificateV3ServiceFactory = ({ actorOrgId, internal = false, removeRootsFromChain - }: TRenewCertificateDTO & { internal?: boolean }): Promise => { + }: Omit & { + internal?: boolean; + }): Promise => { const renewalResult = await certificateDAL.transaction(async (tx) => { const originalCert = await certificateDAL.findById(certificateId, tx); if (!originalCert) { @@ -731,25 +1429,45 @@ export const certificateV3ServiceFactory = ({ }); } - const originalSignatureAlgorithm = originalCert.signatureAlgorithm as CertSignatureAlgorithm; - const originalKeyAlgorithm = originalCert.keyAlgorithm as CertKeyAlgorithm; + // Validate and cast algorithms with fallbacks + let originalSignatureAlgorithm = Object.values(CertSignatureAlgorithm).includes( + originalCert.signatureAlgorithm as CertSignatureAlgorithm + ) + ? (originalCert.signatureAlgorithm as CertSignatureAlgorithm) + : CertSignatureAlgorithm.RSA_SHA256; + let originalKeyAlgorithm = Object.values(CertKeyAlgorithm).includes(originalCert.keyAlgorithm as CertKeyAlgorithm) + ? (originalCert.keyAlgorithm as CertKeyAlgorithm) + : CertKeyAlgorithm.RSA_2048; + // For external CA certificates without stored algorithm info, extract from certificate if (!originalSignatureAlgorithm || !originalKeyAlgorithm) { - throw new BadRequestError({ - message: - "Original certificate does not have algorithm information stored. Cannot renew certificate issued before algorithm tracking was implemented." - }); + const isExternalCA = originalCert.caId && !originalCert.caId.startsWith("internal"); + + if (isExternalCA) { + // For external CA certificates, we can extract algorithm info from the cert or use defaults + originalSignatureAlgorithm = originalSignatureAlgorithm || CertSignatureAlgorithm.RSA_SHA256; + originalKeyAlgorithm = originalKeyAlgorithm || CertKeyAlgorithm.RSA_2048; + } else { + throw new BadRequestError({ + message: + "Original certificate does not have algorithm information stored. Cannot renew certificate issued before algorithm tracking was implemented." + }); + } } - const profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId); - if (!profile) { - throw new NotFoundError({ message: "Certificate profile not found" }); - } + let profile = null; + if (originalCert.profileId) { + profile = await certificateProfileDAL.findByIdWithConfigs(originalCert.profileId); + if (!profile) { + throw new NotFoundError({ message: "Certificate profile not found" }); + } - if (profile.enrollmentType !== EnrollmentType.API) { - throw new ForbiddenRequestError({ - message: "Certificate is not eligible for renewal: EST certificates cannot be renewed through this endpoint" - }); + if (profile.enrollmentType !== EnrollmentType.API) { + throw new ForbiddenRequestError({ + message: + "Certificate is not eligible for renewal: Only certificates issued from an API enrollment profile can be renewed through this endpoint" + }); + } } const certificateSecret = await certificateSecretDAL.findOne({ certId: originalCert.id }, tx); @@ -761,10 +1479,11 @@ export const certificateV3ServiceFactory = ({ } if (!internal) { + const projectId = profile?.projectId || originalCert.projectId; const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId: profile.projectId, + projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager @@ -776,33 +1495,49 @@ export const certificateV3ServiceFactory = ({ ); } - const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); - if (!ca) { - throw new NotFoundError({ message: "Certificate Authority not found" }); + const issuerType = profile?.issuerType || (originalCert.caId ? IssuerType.CA : IssuerType.SELF_SIGNED); + + let ca; + if (issuerType === IssuerType.CA) { + const caId = profile?.caId || originalCert.caId; + if (!caId) { + throw new NotFoundError({ message: "Certificate Authority ID not found" }); + } + + ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca) { + throw new NotFoundError({ message: "Certificate Authority not found" }); + } + + const eligibilityCheck = validateRenewalEligibility(originalCert, ca); + if (!eligibilityCheck.isEligible) { + await certificateDAL.updateById(originalCert.id, { + renewalError: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}` + }); + throw new BadRequestError({ + message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}` + }); + } + + const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; + if (caType === CaType.INTERNAL) { + validateCaSupport(ca, "direct certificate issuance"); + } } - const eligibilityCheck = validateRenewalEligibility(originalCert, ca); - if (!eligibilityCheck.isEligible) { - await certificateDAL.updateById(originalCert.id, { - renewalError: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}` - }); - throw new BadRequestError({ - message: `Certificate is not eligible for renewal: ${eligibilityCheck.errors.join(", ")}` - }); - } + const templateId = profile?.certificateTemplateId || originalCert.certificateTemplateId; + const template = templateId + ? await certificateTemplateV2Service.getTemplateV2ById({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + templateId, + internal + }) + : null; - validateCaSupport(ca, "direct certificate issuance"); - - const template = await certificateTemplateV2Service.getTemplateV2ById({ - actor, - actorId, - actorAuthMethod, - actorOrgId, - templateId: profile.certificateTemplateId, - internal - }); - - if (!template) { + if (!template && profile) { throw new NotFoundError({ message: "Certificate template not found for this profile" }); } @@ -813,42 +1548,10 @@ export const certificateV3ServiceFactory = ({ const certificateRequest = { commonName: originalCert.commonName || undefined, - keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)), - extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy( - parseExtendedKeyUsages(originalCert.extendedKeyUsages) - ), + keyUsages: parseKeyUsages(originalCert.keyUsages), + extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages), subjectAlternativeNames: originalCert.altNames - ? originalCert.altNames.split(",").map((san) => { - const trimmed = san.trim(); - - const isIpv4 = new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(trimmed); - const isIpv6 = new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(trimmed); - if (isIpv4 || isIpv6) { - return { - type: CertSubjectAlternativeNameType.IP_ADDRESS, - value: trimmed - }; - } - - if (new RE2("^[^@]+@[^@]+\\.[^@]+$").test(trimmed)) { - return { - type: CertSubjectAlternativeNameType.EMAIL, - value: trimmed - }; - } - - if (new RE2("^[a-zA-Z][a-zA-Z0-9+.-]*:").test(trimmed)) { - return { - type: CertSubjectAlternativeNameType.URI, - value: trimmed - }; - } - - return { - type: CertSubjectAlternativeNameType.DNS_NAME, - value: trimmed - }; - }) + ? originalCert.altNames.split(",").map((san) => detectSanType(san.trim())) : [], validity: { ttl @@ -857,10 +1560,13 @@ export const certificateV3ServiceFactory = ({ keyAlgorithm: originalCert.keyAlgorithm || undefined }; - const validationResult = await certificateTemplateV2Service.validateCertificateRequest( - profile.certificateTemplateId, - certificateRequest - ); + let validationResult: { isValid: boolean; errors: string[] } = { isValid: true, errors: [] }; + if (profile?.certificateTemplateId) { + validationResult = await certificateTemplateV2Service.validateCertificateRequest( + profile.certificateTemplateId, + certificateRequest + ); + } if (!validationResult.isValid) { await certificateDAL.updateById(originalCert.id, { @@ -872,48 +1578,139 @@ export const certificateV3ServiceFactory = ({ }); } - validateAlgorithmCompatibility(ca, template); const notBefore = new Date(); const notAfter = new Date(Date.now() + parseTtlToDays(ttl) * 24 * 60 * 60 * 1000); - const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, ttl, notAfter); + const finalRenewBeforeDays = profile ? calculateFinalRenewBeforeDays(profile, ttl, notAfter) : undefined; - const { certificate, certificateChain, issuingCaCertificate, serialNumber } = - await internalCaService.issueCertFromCa({ - caId: ca.id, - friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate", - commonName: originalCert.commonName || "", - altNames: originalCert.altNames || "", - ttl, - notBefore: normalizeDateForApi(notBefore), - notAfter: normalizeDateForApi(notAfter), - keyUsages: parseKeyUsages(originalCert.keyUsages), - extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages), - signatureAlgorithm: originalSignatureAlgorithm, - keyAlgorithm: originalKeyAlgorithm, - isFromProfile: true, - actor, - actorId, - actorAuthMethod, - actorOrgId, - internal: true, - tx + let certificate: string; + let certificateChain: string; + let issuingCaCertificate: string; + let serialNumber: string; + let newCert: TCertificates; + + if (issuerType === IssuerType.CA) { + // CA-signed certificate renewal + if (!ca) { + throw new NotFoundError({ message: "Certificate Authority not found for CA-signed certificate renewal" }); + } + + const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; + + // Only validate algorithm compatibility for internal CAs + if (caType === CaType.INTERNAL) { + validateAlgorithmCompatibility(ca, { + algorithms: template?.algorithms + } as { algorithms?: { signature?: string[] } }); + } + + if (caType === CaType.INTERNAL) { + // Internal CA renewal - existing logic + const caResult = await internalCaService.issueCertFromCa({ + caId: ca.id, + friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate", + commonName: originalCert.commonName || "", + altNames: originalCert.altNames || "", + ttl, + notBefore: normalizeDateForApi(notBefore), + notAfter: normalizeDateForApi(notAfter), + keyUsages: convertKeyUsageArrayToLegacy(parseKeyUsages(originalCert.keyUsages)), + extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy( + parseExtendedKeyUsages(originalCert.extendedKeyUsages) + ), + signatureAlgorithm: originalSignatureAlgorithm, + keyAlgorithm: originalKeyAlgorithm, + isFromProfile: true, + actor, + actorId, + actorAuthMethod, + actorOrgId, + internal: true, + tx + }); + + certificate = caResult.certificate; + certificateChain = caResult.certificateChain; + issuingCaCertificate = caResult.issuingCaCertificate; + serialNumber = caResult.serialNumber; + + const foundCert = await certificateDAL.findById(caResult.certificateId, tx); + if (!foundCert) { + throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); + } + newCert = foundCert; + } else if (caType === CaType.ACME || caType === CaType.AZURE_AD_CS) { + // External CA renewal - mark for async processing outside transaction + return { + isExternalCA: true, + ca, + profile, + originalCert, + originalSignatureAlgorithm, + originalKeyAlgorithm, + ttl + }; + } else { + throw new BadRequestError({ + message: `CA type ${String(caType)} does not support certificate renewal` + }); + } + } else { + // Self-signed certificate renewal + const effectiveAlgorithms = getEffectiveAlgorithms( + undefined, + undefined, + originalSignatureAlgorithm, + originalKeyAlgorithm + ); + + const selfSignedRenewalResult = await processSelfSignedCertificate({ + certificateRequest, + template, + profile, + originalCert, + effectiveAlgorithms, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + projectDAL, + tx, + isRenewal: true }); - const newCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx); + certificate = selfSignedRenewalResult.selfSignedResult.certificate.toString("utf8"); + certificateChain = selfSignedRenewalResult.selfSignedResult.certificate.toString("utf8"); // Self-signed has no chain + issuingCaCertificate = ""; // No issuing CA for self-signed + serialNumber = selfSignedRenewalResult.selfSignedResult.serialNumber; + newCert = selfSignedRenewalResult.certificateData; + } + if (!newCert) { throw new NotFoundError({ message: "Certificate was signed but could not be found in database" }); } - await certificateDAL.updateById( - newCert.id, - { - profileId: originalCert.profileId, - renewBeforeDays: finalRenewBeforeDays, + // For self-signed certificates, we already set the renewal data during creation + // For CA-signed certificates, we need to set it now + if (issuerType === IssuerType.CA) { + const renewalUpdateData: { + profileId: string | null; + renewedFromCertificateId: string; + renewBeforeDays?: number; + } = { + profileId: originalCert.profileId || null, renewedFromCertificateId: originalCert.id - }, - tx - ); + }; + + if (finalRenewBeforeDays !== undefined) { + renewalUpdateData.renewBeforeDays = finalRenewBeforeDays; + } + + await certificateDAL.updateById(newCert.id, renewalUpdateData, tx); + } else if (finalRenewBeforeDays !== undefined) { + // For self-signed certificates, just update the renewBeforeDays if needed + await certificateDAL.updateById(newCert.id, { renewBeforeDays: finalRenewBeforeDays }, tx); + } await certificateDAL.updateById( originalCert.id, @@ -926,6 +1723,28 @@ export const certificateV3ServiceFactory = ({ await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx); + const certRequestResult = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: originalCert.projectId, + tx, + caId: ca?.id || originalCert.caId || undefined, + profileId: originalCert.profileId || undefined, + commonName: originalCert.commonName || undefined, + altNames: originalCert.altNames || undefined, + keyUsages: parseKeyUsages(originalCert.keyUsages), + extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages), + notBefore: new Date(newCert.notBefore), + notAfter: new Date(newCert.notAfter), + keyAlgorithm: originalKeyAlgorithm, + signatureAlgorithm: originalSignatureAlgorithm, + metadata: `Renewed from certificate ID: ${originalCert.id}`, + status: CertificateRequestStatus.ISSUED, + certificateId: newCert.id + }); + return { certificate, certificateChain, @@ -933,10 +1752,76 @@ export const certificateV3ServiceFactory = ({ serialNumber, newCert, originalCert, - profile + profile, + certRequestResult }; }); + let certificateRequestId: string = renewalResult.certRequestResult?.id || ""; + + // Handle external CA renewals separately + if ("isExternalCA" in renewalResult && renewalResult.isExternalCA) { + const { ca, profile, originalCert, originalSignatureAlgorithm, originalKeyAlgorithm, ttl } = renewalResult; + + const renewalOrderId = randomUUID(); + const altNamesArray = originalCert.altNames + ? originalCert.altNames.split(",").map((san: string) => san.trim()) + : []; + + const certificateRequest = await certificateRequestService.createCertificateRequest({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId: originalCert.projectId, + profileId: profile?.id, + caId: ca.id, + commonName: originalCert.commonName || undefined, + altNames: originalCert.altNames || undefined, + keyUsages: parseKeyUsages(originalCert.keyUsages), + extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages), + keyAlgorithm: originalKeyAlgorithm, + signatureAlgorithm: originalSignatureAlgorithm, + metadata: `Renewed from certificate ID: ${originalCert.id}`, + status: CertificateRequestStatus.PENDING + }); + + certificateRequestId = certificateRequest.id; + + await certificateIssuanceQueue.queueCertificateIssuance({ + certificateId: renewalOrderId, + profileId: profile?.id || "", + caId: ca.id, + commonName: originalCert.commonName || "", + altNames: altNamesArray, + ttl, + signatureAlgorithm: originalSignatureAlgorithm, + keyAlgorithm: originalKeyAlgorithm, + keyUsages: convertEnumsToStringArray(parseKeyUsages(originalCert.keyUsages)), + extendedKeyUsages: convertEnumsToStringArray(parseExtendedKeyUsages(originalCert.extendedKeyUsages)), + isRenewal: true, + originalCertificateId: certificateId, + certificateRequestId: certificateRequest.id + }); + + return { + certificate: "", // External CA renewal is async + certificateChain: "", + issuingCaCertificate: "", + serialNumber: "", + certificateId: renewalOrderId, + certificateRequestId: certificateRequest.id, + projectId: originalCert.projectId, + profileName: profile?.slug || "External CA Profile", + commonName: originalCert.commonName || "" + }; + } + + // Type check to ensure we have internal CA renewal result + if ("isExternalCA" in renewalResult) { + throw new BadRequestError({ message: "External CA renewals should be handled asynchronously" }); + } + await triggerAutoSyncForCertificate(renewalResult.newCert.id, { certificateSyncDAL, pkiSyncDAL, @@ -953,8 +1838,9 @@ export const certificateV3ServiceFactory = ({ certificateChain: finalCertificateChain, serialNumber: renewalResult.serialNumber, certificateId: renewalResult.newCert.id, - projectId: renewalResult.profile.projectId, - profileName: renewalResult.profile.slug, + certificateRequestId, + projectId: renewalResult.originalCert.projectId, + profileName: renewalResult.profile?.slug || "Self-signed Certificate", commonName: renewalResult.originalCert.commonName || "" }; }; diff --git a/backend/src/services/certificate-v3/certificate-v3-types.ts b/backend/src/services/certificate-v3/certificate-v3-types.ts index ab638c5ed..50d710406 100644 --- a/backend/src/services/certificate-v3/certificate-v3-types.ts +++ b/backend/src/services/certificate-v3/certificate-v3-types.ts @@ -1,6 +1,5 @@ import { TProjectPermission } from "@app/lib/types"; -import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types"; import { CertExtendedKeyUsageType, CertKeyUsageType, @@ -45,7 +44,7 @@ export type TOrderCertificateFromProfileDTO = { profileId: string; certificateOrder: { altNames: Array<{ - type: ACMESANType; + type: CertSubjectAlternativeNameType; value: string; }>; validity: { @@ -58,6 +57,8 @@ export type TOrderCertificateFromProfileDTO = { notAfter?: Date; signatureAlgorithm?: string; keyAlgorithm?: string; + template?: string; + csr?: string; }; removeRootsFromChain?: boolean; } & Omit; @@ -69,34 +70,14 @@ export type TCertificateFromProfileResponse = { privateKey?: string; serialNumber: string; certificateId: string; + certificateRequestId: string; projectId: string; profileName: string; commonName: string; }; export type TCertificateOrderResponse = { - orderId: string; - status: CertificateOrderStatus; - subjectAlternativeNames: Array<{ - type: ACMESANType; - value: string; - status: CertificateOrderStatus; - }>; - authorizations: Array<{ - identifier: { - type: ACMESANType; - value: string; - }; - status: CertificateOrderStatus; - expires?: string; - challenges: Array<{ - type: string; - status: CertificateOrderStatus; - url: string; - token: string; - }>; - }>; - finalize: string; + certificateRequestId: string; certificate?: string; projectId: string; profileName: string; @@ -105,6 +86,7 @@ export type TCertificateOrderResponse = { export type TRenewCertificateDTO = { certificateId: string; removeRootsFromChain?: boolean; + certificateRequestId?: string; } & Omit; export type TUpdateRenewalConfigDTO = { diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index b632e76fb..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, @@ -309,6 +314,14 @@ export const certificateServiceFactory = ({ const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); + if (!certBody) { + throw new NotFoundError({ message: "Certificate body not found" }); + } + + if (!certBody.encryptedCertificate) { + throw new BadRequestError({ message: "Certificate data not available" }); + } + const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ projectId: cert.projectId, projectDAL, @@ -576,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, @@ -599,6 +619,14 @@ export const certificateServiceFactory = ({ const certBody = await certificateBodyDAL.findOne({ certId: cert.id }); + if (!certBody) { + throw new NotFoundError({ message: "Certificate body not found" }); + } + + if (!certBody.encryptedCertificate) { + throw new BadRequestError({ message: "Certificate data not available" }); + } + const certificateManagerKeyId = await getProjectKmsCertificateKeyId({ projectId: cert.projectId, projectDAL, @@ -657,12 +685,13 @@ export const certificateServiceFactory = ({ certificate, certificateChain, privateKey, - serialNumber, + serialNumber: cert.serialNumber, cert }; }; const getCertPkcs12 = async ({ + id, serialNumber, password, alias, @@ -684,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, @@ -702,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/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index bdc8ab1c1..1a20b1192 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -621,48 +621,61 @@ export const identityTokenAuthServiceFactory = ({ const getTokenAuthTokenById = async ({ tokenId, - identityId, - isActorSuperAdmin, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthTokenByIdDTO) => { - await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + const foundToken = await identityAccessTokenDAL.findOne({ + [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, + [`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH + }); + if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` }); const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ scopeData: { scope: AccessScope.Organization, orgId: actorOrgId }, - identityId + identityId: foundToken.identityId }); - if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (!identityMembershipOrg) { + throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission({ - scope: OrganizationActionScope.Any, - actor, - actorId, - orgId: identityMembershipOrg.scopeOrgId, - actorAuthMethod, - actorOrgId - }); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const token = await identityAccessTokenDAL.findOne({ - [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, - [`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH, - [`${TableName.IdentityAccessToken}.identityId` as "identityId"]: identityId - }); + if (identityMembershipOrg.identity.projectId) { + const { permission } = await permissionService.getProjectPermission({ + actionProjectType: ActionProjectType.Any, + actor, + actorId, + projectId: identityMembershipOrg.identity.projectId, + actorAuthMethod, + actorOrgId + }); - if (!token) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + subject(ProjectPermissionSub.Identity, { identityId: identityMembershipOrg.identity.id }) + ); + } else { + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: identityMembershipOrg.scopeOrgId, + actorAuthMethod, + actorOrgId + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + } - return { token, identityMembershipOrg }; + return { token: foundToken, identityMembershipOrg }; }; const updateTokenAuthToken = async ({ diff --git a/backend/src/services/identity-token-auth/identity-token-auth-types.ts b/backend/src/services/identity-token-auth/identity-token-auth-types.ts index fdecc6d4c..6be2c5fe0 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-types.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-types.ts @@ -42,8 +42,6 @@ export type TGetTokenAuthTokensDTO = { export type TGetTokenAuthTokenByIdDTO = { tokenId: string; - identityId: string; - isActorSuperAdmin?: boolean; } & Omit; export type TUpdateTokenAuthTokenDTO = { 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-group/membership-group-service.ts b/backend/src/services/membership-group/membership-group-service.ts index 0aedccd15..767daab31 100644 --- a/backend/src/services/membership-group/membership-group-service.ts +++ b/backend/src/services/membership-group/membership-group-service.ts @@ -93,6 +93,7 @@ export const membershipGroupServiceFactory = ({ } const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + await factory.onCreateMembershipGroupGuard(dto); const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); @@ -112,6 +113,19 @@ export const membershipGroupServiceFactory = ({ const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); const membership = await membershipGroupDAL.transaction(async (tx) => { + const existingMembership = await membershipGroupDAL.findOne( + { + scope: scopeData.scope, + ...scopeDatabaseFields, + actorGroupId: dto.data.groupId + }, + tx + ); + if (existingMembership) + throw new BadRequestError({ + message: "Group is already a member" + }); + const doc = await membershipGroupDAL.create( { scope: scopeData.scope, 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/membership-identity/membership-identity-service.ts b/backend/src/services/membership-identity/membership-identity-service.ts index ab63c4508..b1dd6e238 100644 --- a/backend/src/services/membership-identity/membership-identity-service.ts +++ b/backend/src/services/membership-identity/membership-identity-service.ts @@ -105,6 +105,19 @@ export const membershipIdentityServiceFactory = ({ const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); const membership = await membershipIdentityDAL.transaction(async (tx) => { + const existingMembership = await membershipIdentityDAL.findOne( + { + scope: scopeData.scope, + ...scopeDatabaseFields, + actorIdentityId: dto.data.identityId + }, + tx + ); + if (existingMembership) + throw new BadRequestError({ + message: "Identity is already a member" + }); + const doc = await membershipIdentityDAL.create( { scope: scopeData.scope, diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index d21b27b69..deb819c7a 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -129,7 +129,7 @@ export const newOrgMembershipUserFactory = ({ recipients: emails as string[], substitutions: { subOrganizationName: orgDetails.slug, - callback_url: `${appCfg.SITE_URL}/organization/projects?subOrganization=${orgDetails.slug}` + callback_url: `${appCfg.SITE_URL}/organizations/${dto.permission.orgId}/projects?subOrganization=${orgDetails.slug}` } }); } else { diff --git a/backend/src/services/microsoft-teams/microsoft-teams-fns.ts b/backend/src/services/microsoft-teams/microsoft-teams-fns.ts index e940fda54..0fcfbbe01 100644 --- a/backend/src/services/microsoft-teams/microsoft-teams-fns.ts +++ b/backend/src/services/microsoft-teams/microsoft-teams-fns.ts @@ -357,7 +357,7 @@ export const isBotInstalledInTenant = async ( } }; -export const buildTeamsPayload = (notification: TNotification) => { +export const buildTeamsPayload = (orgId: string, notification: TNotification) => { const appCfg = getConfig(); switch (notification.type) { @@ -402,7 +402,7 @@ export const buildTeamsPayload = (notification: TNotification) => { { type: "Action.OpenUrl", title: "View request in Infisical", - url: `${appCfg.SITE_URL}/projects/secret-management/${payload.projectId}/approval?requestId=${payload.requestId}` + url: `${appCfg.SITE_URL}/organizations/${orgId}/projects/secret-management/${payload.projectId}/approval?requestId=${payload.requestId}` } ] }; @@ -590,10 +590,11 @@ export class TeamsBot extends TeamsActivityHandler { tenantId: string, channelId: string, teamId: string, + orgId: string, notification: TNotification ) { try { - const { adaptiveCard } = buildTeamsPayload(notification); + const { adaptiveCard } = buildTeamsPayload(orgId, notification); const adaptiveCardActivity = { type: "message", diff --git a/backend/src/services/microsoft-teams/microsoft-teams-service.ts b/backend/src/services/microsoft-teams/microsoft-teams-service.ts index ff17daa75..a9d1af840 100644 --- a/backend/src/services/microsoft-teams/microsoft-teams-service.ts +++ b/backend/src/services/microsoft-teams/microsoft-teams-service.ts @@ -759,7 +759,7 @@ export const microsoftTeamsServiceFactory = ({ }); for await (const channelId of target.channelIds) { - await teamsBot.sendMessageToChannel(botAccessToken, tenantId, channelId, target.teamId, notification); + await teamsBot.sendMessageToChannel(botAccessToken, tenantId, channelId, target.teamId, orgId, notification); } }; 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/project/project-service.ts b/backend/src/services/project/project-service.ts index 188c985fb..d30462e9b 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1984,7 +1984,7 @@ export const projectServiceFactory = ({ projectTypeUrl = "cert-management"; } - const callbackPath = `/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`; + const callbackPath = `/organizations/${project.orgId}/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`; await notificationService.createUserNotifications( projectMembers 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-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 87dd207f1..67170e5f6 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -391,7 +391,7 @@ export const secretSharingServiceFactory = ({ substitutions: { name: secretRequest.name, respondentUsername, - secretRequestUrl: `${appCfg.SITE_URL}/organization/secret-sharing?selectedTab=request-secret` + secretRequestUrl: `${appCfg.SITE_URL}/organizations/${secretRequest.orgId}/secret-sharing?selectedTab=request-secret` }, template: SmtpTemplates.SecretRequestCompleted }); diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index f6e23dded..fd15dc029 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -932,7 +932,7 @@ export const secretSyncQueueFactory = ({ break; } - const baseProjectPath = `/projects/secret-management/${projectId}`; + const baseProjectPath = `/organizations/${project.orgId}/projects/secret-management/${projectId}`; const overviewPath = `${baseProjectPath}/overview`; const syncPath = `${baseProjectPath}/integrations/secret-syncs/${destination}/${secretSync.id}`; 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 61507d127..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", { @@ -742,7 +746,7 @@ export const secretQueueFactory = ({ environment: jobPayload.environmentName, count: jobPayload.count, projectName: project.name, - integrationUrl: `${appCfg.SITE_URL}/projects/secret-management/${project.id}/integrations?selectedTab=native-integrations` + integrationUrl: `${appCfg.SITE_URL}/organizations/${project.orgId}/projects/secret-management/${project.id}/integrations?selectedTab=native-integrations` } }); } @@ -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-dal.ts b/backend/src/services/service-token/service-token-dal.ts index adb2f325a..ae2cd3574 100644 --- a/backend/src/services/service-token/service-token-dal.ts +++ b/backend/src/services/service-token/service-token-dal.ts @@ -30,28 +30,35 @@ export const serviceTokenDALFactory = (db: TDbClient) => { const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0) => { try { - const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] = - await (tx || db.replicaNode())(TableName.ServiceToken) - .leftJoin( - TableName.Users, - `${TableName.Users}.id`, - db.raw(`${TableName.ServiceToken}."createdBy"::uuid`) - ) - .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`) - .whereRaw( - `${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false` - ) - .whereNotNull(`${TableName.Users}.email`) - .select( - db.ref("id").withSchema(TableName.ServiceToken), - db.ref("name").withSchema(TableName.ServiceToken), - db.ref("projectId").withSchema(TableName.ServiceToken), - db.ref("createdBy").withSchema(TableName.ServiceToken), - db.ref("email").withSchema(TableName.Users).as("createdByEmail"), - db.ref("name").withSchema(TableName.Project).as("projectName") - ) - .limit(batchSize) - .offset(offset); + const batch: { + name: string; + projectName: string; + createdByEmail: string; + id: string; + projectId: string; + orgId: string; + }[] = await (tx || db.replicaNode())(TableName.ServiceToken) + .leftJoin( + TableName.Users, + `${TableName.Users}.id`, + db.raw(`${TableName.ServiceToken}."createdBy"::uuid`) + ) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`) + .whereRaw( + `${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false` + ) + .whereNotNull(`${TableName.Users}.email`) + .select( + db.ref("id").withSchema(TableName.ServiceToken), + db.ref("name").withSchema(TableName.ServiceToken), + db.ref("projectId").withSchema(TableName.ServiceToken), + db.ref("createdBy").withSchema(TableName.ServiceToken), + db.ref("email").withSchema(TableName.Users).as("createdByEmail"), + db.ref("name").withSchema(TableName.Project).as("projectName"), + db.ref("orgId").withSchema(TableName.Project).as("orgId") + ) + .limit(batchSize) + .offset(offset); return batch; } catch (err) { diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 081b99208..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) => { @@ -225,16 +227,22 @@ export const serviceTokenServiceFactory = ({ substitutions: { tokenName: token.name, projectName: token.projectName, - url: `${appCfg.SITE_URL}/projects/secret-management/${token.projectId}/access-management?selectedTab=service-tokens` + url: `${appCfg.SITE_URL}/organizations/${token.orgId}/projects/secret-management/${token.projectId}/access-management?selectedTab=service-tokens` } }); - await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true }); + 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/OrgAdminBreakglassAccessTemplate.tsx b/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx index ee09574b6..97ce9b522 100644 --- a/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx +++ b/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx @@ -7,6 +7,7 @@ import { BaseLink } from "./BaseLink"; interface OrgAdminBreakglassAccessTemplateProps extends Omit { email: string; timestamp: string; + orgId: string; ip: string; userAgent: string; } @@ -15,6 +16,7 @@ export const OrgAdminBreakglassAccessTemplate = ({ email, siteUrl, timestamp, + orgId, ip, userAgent }: OrgAdminBreakglassAccessTemplateProps) => { @@ -36,7 +38,7 @@ export const OrgAdminBreakglassAccessTemplate = ({ {userAgent} If you'd like to disable Admin SSO Bypass, please visit{" "} - Organization Security Settings. + Organization Security Settings. @@ -51,5 +53,6 @@ OrgAdminBreakglassAccessTemplate.PreviewProps = { "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15", timestamp: "Tue Apr 29 2025 23:03:27 GMT+0000 (Coordinated Universal Time)", siteUrl: "https://infisical.com", - email: "august@infisical.com" + email: "august@infisical.com", + orgId: "123" } as OrgAdminBreakglassAccessTemplateProps; 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/endpoints/token-auth/get-token.mdx b/docs/api-reference/endpoints/token-auth/get-token.mdx new file mode 100644 index 000000000..69bc14cdc --- /dev/null +++ b/docs/api-reference/endpoints/token-auth/get-token.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Token" +openapi: "GET /api/v1/auth/token-auth/tokens/{tokenId}" +--- 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 76f4fb6cd..c59ad89b7 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", @@ -492,6 +493,10 @@ "pages": [ "integrations/platforms/ansible", "integrations/platforms/apache-airflow", + { + "group": "AWS", + "pages": ["integrations/platforms/aws/lambda"] + }, { "group": "Kubernetes Operator", "pages": [ @@ -570,71 +575,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" ] }, { @@ -754,7 +702,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", @@ -829,7 +777,23 @@ "group": "Infisical PAM", "pages": [ "documentation/platform/pam/overview", - "documentation/platform/pam/session-recording" + { + "group": "Getting Started", + "pages": [ + "documentation/platform/pam/getting-started/setup", + "documentation/platform/pam/getting-started/resources", + "documentation/platform/pam/getting-started/accounts" + ] + }, + "documentation/platform/pam/architecture" + ] + }, + { + "group": "Product Reference", + "pages": [ + "documentation/platform/pam/product-reference/auditing", + "documentation/platform/pam/product-reference/session-recording", + "documentation/platform/pam/product-reference/credential-rotation" ] } ] @@ -886,11 +850,7 @@ "group": "Overview", "pages": [ "api-reference/overview/introduction", - "api-reference/overview/authentication", - { - "group": "Examples", - "pages": ["api-reference/overview/examples/integration"] - } + "api-reference/overview/authentication" ] }, { @@ -2517,20 +2477,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" - ] } ] }, @@ -2557,21 +2503,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" + } ] }, { @@ -2593,23 +2534,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" ] }, { @@ -2625,6 +2554,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/pam/architecture.mdx b/docs/documentation/platform/pam/architecture.mdx new file mode 100644 index 000000000..5f5a12433 --- /dev/null +++ b/docs/documentation/platform/pam/architecture.mdx @@ -0,0 +1,77 @@ +--- +title: "Architecture" +sidebarTitle: "Architecture" +description: "Learn about the architecture, components, and security model of Infisical PAM." +--- + +Infisical PAM utilizes a secure, proxy-based architecture designed to provide access to private resources without exposing them directly to the internet. This system relies on a combination of the Infisical CLI, a Relay server, and a self-hosted Gateway. For more information on Gateways, refer to the [Gateway Overview](/documentation/platform/gateways/overview). + +## Core Components + +The architecture consists of three main components working in unison: + + + + The client-side interface used to initiate access requests. It creates a local listener that forwards traffic securely to the Gateway. + + + A lightweight service deployed within your private network (e.g., VPC, on-prem). It acts as a proxy, intercepting traffic to enforce policies and record sessions before forwarding requests to the target resource. + + + The actual infrastructure being accessed, such as a PostgreSQL database, a Linux server, or a web application. + + + +## Access Flow + +```mermaid +graph LR + subgraph Client ["User Environment"] + CLI["Infisical CLI"] + end + + Relay["Relay Server"] + + subgraph Network ["Private Network (VPC)"] + Gateway["Infisical Gateway"] + DB[("Target Resource (Database/Server)")] + end + + CLI <-->|Encrypted Tunnel| Relay + Relay <-->|Reverse Tunnel| Gateway + Gateway <-->|Native Protocol| DB +``` + +When a user accesses a resource (e.g., via `infisical access`), the following workflow occurs: + +1. **Connection Initiation**: The Infisical CLI initiates a connection to the Relay server. +2. **Tunnel Establishment**: The Relay facilitates an end-to-end encrypted tunnel between the CLI and the Gateway. +3. **Proxy & Credential Injection**: The Gateway authenticates the request and connects to the target resource on the user's behalf. It automatically injects the necessary credentials (e.g., database passwords, SSH keys), ensuring the user never directly handles sensitive secrets. +4. **Traffic Forwarding**: Traffic flows securely from the user's machine, through the Relay, to the Gateway, and finally to the resource. + +## Session Recording & Auditing + +![Session Logging](/images/pam/architecture/session-logging.png) + +A key feature of the Gateway is its ability to act as a "middleman" for all session traffic. + +- **Interception**: Because the Gateway sits between the secure tunnel and the target resource, it intercepts all data flowing through the connection. +- **Logging**: This traffic is logged as part of [Session Recording](/documentation/platform/pam/product-reference/session-recording). The Gateway temporarily stores encrypted session logs locally. +- **Upload**: Once the session concludes, the logs are securely uploaded to the Infisical platform for storage and review. + +## Security Architecture + +The PAM security model allows you to maintain a zero-trust environment while enabling convenient access. + +### End-to-End Encryption +The connection between the Infisical CLI (client) and the Gateway is end-to-end encrypted. The Relay server acts solely as a router for encrypted packets and **cannot decrypt or inspect** the traffic passing through it. + +### Network Security +The Gateway uses **SSH reverse tunnels** to connect to the Relay. This design offers significant security benefits: +- **No Inbound Ports**: You do not need to open any inbound firewall ports (like 22 or 5432) to the internet. +- **Outbound-Only**: The Gateway only requires outbound connectivity to the Relay server and Infisical API. + +For a deep dive into the underlying cryptography, certificate management, and isolation guarantees, refer to the [Gateway Security Architecture](/documentation/platform/gateways/security). + +### Deployment +For instructions on setting up the necessary infrastructure, see the [Gateway Deployment Guide](/documentation/platform/gateways/gateway-deployment). diff --git a/docs/documentation/platform/pam/getting-started/accounts.mdx b/docs/documentation/platform/pam/getting-started/accounts.mdx new file mode 100644 index 000000000..4ed0616f8 --- /dev/null +++ b/docs/documentation/platform/pam/getting-started/accounts.mdx @@ -0,0 +1,47 @@ +--- +title: "PAM Account" +sidebarTitle: "Accounts" +description: "Learn how to create and manage accounts in PAM to control access to resources like databases and servers." +--- + +An **Account** contains the credentials (such as a username and password) used to connect to a [Resource](/documentation/platform/pam/getting-started/resources). + +## Relationship to Resources + +Accounts belong to Resources. A single Resource can have multiple Accounts associated with it, each with different permission levels. + +For example, your database would normally have multiple accounts. You might have a superuser account for admins, a standard read/write account for applications, and a read-only account for reporting. + +In PAM, these are represented as: +- **Resource**: `Production Database` (PostgreSQL) + - **Account 1**: `postgres` (Superuser) + - **Account 2**: `app_user` (Read/Write) + - **Account 3**: `analytics` (Read-only) + +When a user requests access in PAM, they request access to a specific **Account** on a **Resource**. + +## Creating an Account + + + **Prerequisite**: You must have at least one [Resource](/documentation/platform/pam/getting-started/resources) created before adding accounts. + + +To add an account, navigate to the **Accounts** tab in your PAM project and click **Add Account**. + +![Add Account Button](/images/pam/getting-started/accounts/add-account-button.png) + +Next, select the **Resource** that this account belongs to. + +![Select Resource](/images/pam/getting-started/accounts/select-resource.png) + +After selecting a resource, provide the credentials (username, password, etc.) for this account. The required fields vary depending on the resource type. For example, for a Linux server, you would enter the username and the corresponding password or SSH key. + +![Create Account](/images/pam/getting-started/accounts/create-account.png) + +Clicking **Create Account** will trigger a validation check. Infisical will attempt to connect to the resource using the provided credentials to verify they are valid. + +## Automated Credential Rotation + +Infisical supports automated credential rotation for some accounts on select resources, allowing you to automatically change passwords at set intervals to enhance security. + +To learn more about how to configure this, please refer to the [Credential Rotation guide](/documentation/platform/pam/product-reference/credential-rotation). diff --git a/docs/documentation/platform/pam/getting-started/resources.mdx b/docs/documentation/platform/pam/getting-started/resources.mdx new file mode 100644 index 000000000..4eaeebb74 --- /dev/null +++ b/docs/documentation/platform/pam/getting-started/resources.mdx @@ -0,0 +1,45 @@ +--- +title: "PAM Resource" +sidebarTitle: "Resources" +description: "Learn how to add and configure resources like databases and servers, and set up automated credential rotation." +--- + +A resource represents a target system, such as a database, server, or application, that you want to manage access to. Some examples of resources are: +- PostgreSQL Database +- MCP Server +- Linux Server +- Web Application + +## Prerequisites + +Before you can create a resource, you must have an **Infisical Gateway** deployed that is able to reach the target resource over the network. + +The Gateway acts as a secure bridge, allowing Infisical to reach your private infrastructure without exposing it to the public internet. When creating a resource, you will be asked to specify which Gateway should be used to connect to it. + +[Read the Gateway Deployment Guide](/documentation/platform/gateways/gateway-deployment) + +## Creating a Resource + +To add a resource, navigate to the **Resources** tab in your PAM project and click **Add Resource**. + +![Add Resource Button](/images/pam/getting-started/resources/add-resource-button.png) + +Next, select the type of resource you want to add. + +![Select Resource Type](/images/pam/getting-started/resources/select-resource-type.png) + +After selecting a resource type, provide the necessary connection details. The required fields vary depending on the resource type. + +**Important**: You must select the **Gateway** that has network access to this resource. + +In this PostgreSQL example, you provide details such as host, port, gateway, and database name. + +![Create Resource](/images/pam/getting-started/resources/create-resource.png) + +Clicking **Create Resource** will trigger a connection test from the selected Gateway to your target resource. If the connection fails, an error message will be displayed to help you troubleshoot (usually indicating a network firewall issue between the Gateway and the Resource). + +## Automated Credential Rotation + +Some resources, such as PostgreSQL, support automated credential rotation to enhance your security posture. This feature requires configuring a privileged "Rotation Account" on the resource. + +To learn more about how to configure this, please refer to the [Credential Rotation guide](/documentation/platform/pam/product-reference/credential-rotation). diff --git a/docs/documentation/platform/pam/getting-started/setup.mdx b/docs/documentation/platform/pam/getting-started/setup.mdx new file mode 100644 index 000000000..8da923712 --- /dev/null +++ b/docs/documentation/platform/pam/getting-started/setup.mdx @@ -0,0 +1,35 @@ +--- +title: "Setup" +sidebarTitle: "Setup" +description: "This guide provides a step-by-step walkthrough for configuring Infisical's Privileged Access Management (PAM). Learn how to deploy a gateway, define resources, and grant your team secure, audited access to critical infrastructure." +--- + +Infisical's Privileged Access Management (PAM) solution enables you to provide developers with secure, just-in-time access to your critical infrastructure, such as databases, servers, and web applications. Instead of sharing static credentials, your team can request temporary access through Infisical, which is then brokered through a secure gateway with full auditing and session recording. + +Getting started involves a few key components: +- **Gateways:** A lightweight service you deploy in your own infrastructure to act as a secure entry point to your private resources. +- **Resources:** The specific systems you want to manage access to (e.g., a PostgreSQL database or an SSH server). +- **Accounts:** The privileged credentials (e.g., a database user or an SSH user) that Infisical uses to connect to a resource on behalf of a user. + +The following steps will guide you through the entire setup process, from deploying your first gateway to establishing a secure connection. + + + + Before you can manage any resources, you must deploy an **Infisical Gateway** within your infrastructure. This component is responsible for brokering connections to your private resources. + + [Read the Gateway Deployment Guide](/documentation/platform/gateways/gateway-deployment) + + + Once the Gateway is active, define a **Resource** in Infisical (e.g., "Production Database"). You will link this resource to your deployed Gateway so Infisical knows how to reach it. + + [Learn about Resources](/documentation/platform/pam/getting-started/resources) + + + Add **Accounts** to your Resource (e.g., `postgres` or `read_only_user`). These represent the actual PAM users or privileged identities that are utilized when a user connects. + + [Learn about Accounts](/documentation/platform/pam/getting-started/accounts) + + + Users can now use the Infisical CLI to securely connect to the resource using the defined accounts, with full auditing and session recording enabled. + + diff --git a/docs/documentation/platform/pam/overview.mdx b/docs/documentation/platform/pam/overview.mdx index a6e0094f5..2b311c48c 100644 --- a/docs/documentation/platform/pam/overview.mdx +++ b/docs/documentation/platform/pam/overview.mdx @@ -1,45 +1,67 @@ --- -title: "Infisical PAM" +title: "Overview" sidebarTitle: "Overview" -description: "Learn how to manage access to resources like databases, servers, and accounts with policy-based controls and approvals." +description: "Manage and secure access to critical infrastructure like databases and servers with policy-based controls and approvals." --- Infisical Privileged Access Management (PAM) provides a centralized way to manage and secure access to your critical infrastructure. It allows you to enforce fine-grained, policy-based controls over resources like databases, servers, and more, ensuring that only authorized users can access sensitive systems, and only when they need to. -### How it Works +## The PAM Workflow -Infisical PAM employs a resource-based model to organize and manage access. This model is designed to be intuitive and scalable. +At its core, Infisical PAM is designed to decouple **user identity** from **infrastructure credentials**. Instead of sharing static passwords or SSH keys, users authenticate with their SSO identity, and Infisical handles the rest. -#### 1. Create a Resource +Here is how a typical access lifecycle looks: -The first step is to define a resource you want to manage. A resource represents a target system, such as a PostgreSQL database. When creating a resource, you'll provide the necessary connection details, like the host and port. +1. **Discovery**: A user logs into Infisical and sees a catalog of resources (databases, servers) and accounts they are allowed to access. +2. **Connection**: The user selects a resource and an account (e.g., "Production DB" as `read_only`). They initiate the connection via the Infisical CLI. +3. **Credential Injection**: Infisical validates the request. If allowed, it establishes a secure tunnel and automatically injects the credentials for the target account. **The user never sees the underlying password or key.** +4. **Monitoring**: The session is established. All traffic is intercepted, logged, and recorded for audit purposes. -![Create Resource](/images/pam/overview/create-resource.png) +## Core Concepts -#### 2. Add Accounts to the Resource +To successfully implement Infisical PAM, it is essential to understand the relationship between the following components: -Once a resource is created, you can add accounts to it. An account represents a specific set of credentials (e.g., a username and password) that can be used to access the resource. This allows you to manage multiple sets of credentials for a single database or server from one place. + + + A lightweight service deployed in your network that acts as a secure bridge to your private infrastructure. + + + The specific target you are protecting (e.g., a PostgreSQL database or an Ubuntu server). + + + The specific identity on the Resource that the user is trying to access. One Resource can have multiple Accounts. + + -![Create Account](/images/pam/overview/create-account.png) +### Relationship Model -### Infisical PAM Features +The hierarchy is structured as follows: -#### Session Logging and Auditing +```mermaid +graph TD + GW[Gateway] --> |Provides Access| DB[Resource: Production DB] + GW[Gateway] --> |Provides Access| SRV[Resource: Linux Server] + + DB --> A1[Account: admin] + DB --> A2[Account: readonly] + + SRV --> A3[Account: ubuntu] +``` -- **Session Logging**: All user sessions are extensively logged, providing a detailed and searchable record of activities performed during a session. -- **Audit Logging**: Every significant event, such as a user starting a session or accessing an account's credentials, is recorded in audit logs. This gives you complete visibility over your project. +1. **Gateway**: Deployed once per network/VPC. It provides connectivity to all resources in that environment. +2. **Resource**: Configured within Infisical. It points to a specific IP/Host accessible by the Gateway. +3. **Account**: Defined under a Resource. Users request access to a specific *Account* on a *Resource*. -![Session Page](/images/pam/overview/session-page.png) +## Network Architecture -#### Automated Credential Rotation +Infisical PAM uses a secure proxy-based architecture to connect users to resources without direct network exposure. -Infisical PAM can automatically rotate account credentials to enhance your security posture. +When a user accesses a resource, their connection is routed securely through a Relay to your self-hosted Gateway, which then connects to the target resource. This ensures zero-trust access without exposing your infrastructure to the public internet. -Here’s how it works: -1. **Add a Rotation Account**: On the resource level, you configure a "rotation account." This is a master or privileged account that has the necessary permissions to change the passwords of other accounts on that same resource. -![Credential Rotation Account](/images/pam/overview/credential-rotation-account.png) +For a deep dive into the technical architecture and security model, see [Architecture](/documentation/platform/pam/architecture). -2. **Configure Rotation on Accounts**: For each individual account you want to rotate, you can simply enable rotation and set a desired interval (e.g., every 30 days). -![Rotate Credentials Account](/images/pam/overview/rotate-credentials-account.png) +## Core Capabilities -Infisical will then use the rotation account on the resource to automatically update the credentials of the target account at the specified interval, eliminating credential staleness. +- **[Auditing](/documentation/platform/pam/product-reference/auditing)**: Track and review a comprehensive log of all user actions and system events. +- **[Session Recording](/documentation/platform/pam/product-reference/session-recording)**: Record and playback user sessions for security reviews, compliance, and troubleshooting. +- **[Automated Credential Rotation](/documentation/platform/pam/product-reference/credential-rotation)**: Automatically rotate credentials for supported resources to minimize the risk of compromised credentials. diff --git a/docs/documentation/platform/pam/product-reference/auditing.mdx b/docs/documentation/platform/pam/product-reference/auditing.mdx new file mode 100644 index 000000000..e716b7f76 --- /dev/null +++ b/docs/documentation/platform/pam/product-reference/auditing.mdx @@ -0,0 +1,23 @@ +--- +title: "Auditing" +sidebarTitle: "Auditing" +description: "Learn how Infisical audits all actions across your PAM project." +--- + +## What's Audited + +Infisical logs a wide range of actions to provide a complete audit trail for your PAM project. These actions include: + +- Session Start and End +- Fetching session credentials +- Creating, updating, or deleting resources, accounts, folders, and sessions + + + Please note: Audit logs track metadata about sessions (e.g., start/end times), but not the specific commands executed *within* them. For detailed in-session activity, check out [Session Recording](/documentation/platform/pam/product-reference/session-recording). + + +## Viewing Audit Logs + +You can view, search, and filter all events from the **Audit Logs** page within your PAM project. + +![Audit Logs](/images/pam/product-reference/auditing/audit-logs.png) diff --git a/docs/documentation/platform/pam/product-reference/credential-rotation.mdx b/docs/documentation/platform/pam/product-reference/credential-rotation.mdx new file mode 100644 index 000000000..c3204ff56 --- /dev/null +++ b/docs/documentation/platform/pam/product-reference/credential-rotation.mdx @@ -0,0 +1,47 @@ +--- +title: "Credential Rotation" +sidebarTitle: "Credential Rotation" +description: "Learn how to automate credential rotation for your PAM resources." +--- + +Automated Credential Rotation enhances your security posture by automatically changing the passwords of your accounts at set intervals. This minimizes the risk of compromised credentials by ensuring that even if a password is leaked, it remains valid only for a short period. + +## How it Works + +When rotation is enabled, Infisical's Gateway connects to the target resource using a privileged "Rotation Account". It then executes the necessary commands to change the password for the target user account to a new, cryptographically secure random value. + +## Configuration + +Setting up automated rotation requires a two-step configuration: first at the Resource level, and then at the individual Account level. + + + + A **Rotation Account** is a master or privileged account that has the necessary permissions to change the passwords of other users on the target system. + + When creating or editing a [Resource](/documentation/platform/pam/getting-started/resources), you must provide the credentials for this privileged account. + + *Example: For a PostgreSQL database, this would typically be the `postgres` superuser or another role with `ALTER ROLE` privileges.* + + ![Credential Rotation Account](/images/pam/getting-started/resources/credential-rotation-account.png) + + + + Once the resource has a rotation account configured, you can enable rotation for individual [Accounts](/documentation/platform/pam/getting-started/accounts) that belong to that resource. + + In the account settings: + 1. Toggle **Enable Rotation**. + 2. Set the **Rotation Interval** (e.g., every 7 days, 30 days). + + ![Rotate Credentials Account](/images/pam/getting-started/resources/rotate-credentials-account.png) + + + +## Supported Resources + +Automated rotation is currently supported for the following resource types: + +- **PostgreSQL**: Requires a user with `ALTER ROLE` permissions. + + + We are constantly adding support for more resource types. + diff --git a/docs/documentation/platform/pam/product-reference/session-recording.mdx b/docs/documentation/platform/pam/product-reference/session-recording.mdx new file mode 100644 index 000000000..954f2f992 --- /dev/null +++ b/docs/documentation/platform/pam/product-reference/session-recording.mdx @@ -0,0 +1,60 @@ +--- +title: "Session Recording" +sidebarTitle: "Session Recording" +description: "Learn how Infisical records and stores session activity for auditing and monitoring." +--- + +Infisical PAM provides robust session recording capabilities to help you audit and monitor user activity across your infrastructure. + +## How It Works + +When a user initiates a session by accessing an account, a recording of the session begins. The Gateway securely caches all recording data in temporary encrypted files on its local system. + +Once the session concludes, the gateway transmits the complete recording to the Infisical platform for long-term, centralized storage. This asynchronous process ensures that sessions remain operational even if the connection to the Infisical platform is temporarily lost. After the upload is complete, administrators can search and review the session logs on the Infisical platform. + +## What's Captured + +The content captured during a session depends on the type of resource being accessed. + + + + Infisical captures all queries executed and their corresponding responses, including timestamps for each action. + + + Infisical captures all commands executed and their corresponding responses, including timestamps for each action. + + + +## Viewing Recordings + +To review session recordings: + +1. Navigate to the **Sessions** page in your PAM project. +2. Click on a session from the list to view its details. + +![PAM Sessions](/images/pam/product-reference/session-recording/sessions-page.png) + +The session details page provides key information, including the complete session logs, connection status, the user who initiated it, and more. + +![PAM Individual Session](/images/pam/product-reference/session-recording/individual-session-page.png) + +### Searching Logs + +You can use the search bar to quickly find relevant information: + +**Sessions page:** Search across all session logs to locate specific queries or outputs. +![PAM Sessions Search](/images/pam/product-reference/session-recording/sessions-page-search.png) + +**Individual session page:** Search within that specific session's logs to pinpoint activity. +![PAM Individual Session Search](/images/pam/product-reference/session-recording/individual-session-page-search.png) + +## FAQ + + + + Yes. All session recordings are encrypted at rest by default, ensuring your data is always secure. + + + Currently, Infisical uses an asynchronous approach where the gateway records the entire session locally before uploading it. This design makes your PAM sessions more resilient, as they don't depend on a constant, active connection to the Infisical platform. We may introduce live streaming capabilities in a future release. + + diff --git a/docs/documentation/platform/pam/session-recording.mdx b/docs/documentation/platform/pam/session-recording.mdx deleted file mode 100644 index e9061430c..000000000 --- a/docs/documentation/platform/pam/session-recording.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: "Session Recording" -sidebarTitle: "Session Recording" -description: "Learn how Infisical records and stores session activity for auditing and monitoring." ---- - -Infisical's Privileged Access Management (PAM) provides robust session recording capabilities to help you audit and monitor user activity across your infrastructure. - -## How It Works - -When a user initiates a session through the Infisical Gateway, a recording of the session begins. The gateway securely caches all recording data in temporary encrypted files on its local system. - -Once the session concludes, the gateway transmits the complete recording to the Infisical platform for long-term, centralized storage. This asynchronous process ensures that sessions remain operational even if the connection to the Infisical platform is temporarily lost. After the upload is complete, administrators can search and review the session logs in the Infisical UI. - -## What's Captured - -The content captured during a session depends on the type of resource being accessed. - -### Database Sessions - -For database connections, Infisical captures all queries executed and their corresponding responses. - - -Support for additional resource types like SSH, RDP, Kubernetes, and MCP is coming soon. - - -## Viewing Recordings - -To review session recordings: - -1. Navigate to the **PAM Sessions** page in your project. -2. Click on a session from the list to view its details. - -![PAM Sessions](/images/pam/session-recording/sessions-page.png) - -The session details page provides key information, including the complete session logs, connection status, the user who initiated it, and more. - -![PAM Individual Session](/images/pam/session-recording/individual-session-page.png) - -### Searching Logs - -You can use the search bar to quickly find relevant information: - -- **On the main Sessions page:** Search across all session logs to locate specific queries or outputs. -- **On an individual session page:** Search within that specific session's logs to pinpoint activity. - -![PAM Sessions Search](/images/pam/session-recording/sessions-page-search.png) - -![PAM Individual Session Search](/images/pam/session-recording/individual-session-page-search.png) - -## FAQ - - - - Yes. All session recordings are encrypted at rest by default, ensuring your audit data is always secure. - - - Currently, Infisical uses an asynchronous approach where the gateway records the entire session locally before uploading it. This design makes your PAM sessions more resilient, as they don't depend on a constant, active connection to the Infisical platform. We may introduce live streaming capabilities in a future release. - - 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/pam/architecture/session-logging.png b/docs/images/pam/architecture/session-logging.png new file mode 100644 index 000000000..cc64aad4a Binary files /dev/null and b/docs/images/pam/architecture/session-logging.png differ diff --git a/docs/images/pam/getting-started/accounts/add-account-button.png b/docs/images/pam/getting-started/accounts/add-account-button.png new file mode 100644 index 000000000..7ff6c459e Binary files /dev/null and b/docs/images/pam/getting-started/accounts/add-account-button.png differ diff --git a/docs/images/pam/getting-started/accounts/create-account.png b/docs/images/pam/getting-started/accounts/create-account.png new file mode 100644 index 000000000..af79dc365 Binary files /dev/null and b/docs/images/pam/getting-started/accounts/create-account.png differ diff --git a/docs/images/pam/getting-started/accounts/select-resource.png b/docs/images/pam/getting-started/accounts/select-resource.png new file mode 100644 index 000000000..cba352726 Binary files /dev/null and b/docs/images/pam/getting-started/accounts/select-resource.png differ diff --git a/docs/images/pam/getting-started/resources/add-resource-button.png b/docs/images/pam/getting-started/resources/add-resource-button.png new file mode 100644 index 000000000..0769b572c Binary files /dev/null and b/docs/images/pam/getting-started/resources/add-resource-button.png differ diff --git a/docs/images/pam/getting-started/resources/create-resource.png b/docs/images/pam/getting-started/resources/create-resource.png new file mode 100644 index 000000000..8477d6297 Binary files /dev/null and b/docs/images/pam/getting-started/resources/create-resource.png differ diff --git a/docs/images/pam/getting-started/resources/credential-rotation-account.png b/docs/images/pam/getting-started/resources/credential-rotation-account.png new file mode 100644 index 000000000..f44e5e670 Binary files /dev/null and b/docs/images/pam/getting-started/resources/credential-rotation-account.png differ diff --git a/docs/images/pam/getting-started/resources/rotate-credentials-account.png b/docs/images/pam/getting-started/resources/rotate-credentials-account.png new file mode 100644 index 000000000..8c9113b9b Binary files /dev/null and b/docs/images/pam/getting-started/resources/rotate-credentials-account.png differ diff --git a/docs/images/pam/getting-started/resources/select-resource-type.png b/docs/images/pam/getting-started/resources/select-resource-type.png new file mode 100644 index 000000000..e14b2ab3a Binary files /dev/null and b/docs/images/pam/getting-started/resources/select-resource-type.png differ diff --git a/docs/images/pam/overview/create-account.png b/docs/images/pam/overview/create-account.png deleted file mode 100644 index 34f1c7434..000000000 Binary files a/docs/images/pam/overview/create-account.png and /dev/null differ diff --git a/docs/images/pam/overview/create-resource.png b/docs/images/pam/overview/create-resource.png deleted file mode 100644 index ac34b9dca..000000000 Binary files a/docs/images/pam/overview/create-resource.png and /dev/null differ diff --git a/docs/images/pam/overview/credential-rotation-account.png b/docs/images/pam/overview/credential-rotation-account.png deleted file mode 100644 index 5e379eccc..000000000 Binary files a/docs/images/pam/overview/credential-rotation-account.png and /dev/null differ diff --git a/docs/images/pam/overview/rotate-credentials-account.png b/docs/images/pam/overview/rotate-credentials-account.png deleted file mode 100644 index 3c908cd49..000000000 Binary files a/docs/images/pam/overview/rotate-credentials-account.png and /dev/null differ diff --git a/docs/images/pam/overview/session-page.png b/docs/images/pam/overview/session-page.png deleted file mode 100644 index 5c2fa41cf..000000000 Binary files a/docs/images/pam/overview/session-page.png and /dev/null differ diff --git a/docs/images/pam/product-reference/auditing/audit-logs.png b/docs/images/pam/product-reference/auditing/audit-logs.png new file mode 100644 index 000000000..f8e9d8b3b Binary files /dev/null and b/docs/images/pam/product-reference/auditing/audit-logs.png differ diff --git a/docs/images/pam/product-reference/session-recording/individual-session-page-search.png b/docs/images/pam/product-reference/session-recording/individual-session-page-search.png new file mode 100644 index 000000000..d4f31218d Binary files /dev/null and b/docs/images/pam/product-reference/session-recording/individual-session-page-search.png differ diff --git a/docs/images/pam/product-reference/session-recording/individual-session-page.png b/docs/images/pam/product-reference/session-recording/individual-session-page.png new file mode 100644 index 000000000..2efd60312 Binary files /dev/null and b/docs/images/pam/product-reference/session-recording/individual-session-page.png differ diff --git a/docs/images/pam/product-reference/session-recording/sessions-page-search.png b/docs/images/pam/product-reference/session-recording/sessions-page-search.png new file mode 100644 index 000000000..eaebbd70a Binary files /dev/null and b/docs/images/pam/product-reference/session-recording/sessions-page-search.png differ diff --git a/docs/images/pam/product-reference/session-recording/sessions-page.png b/docs/images/pam/product-reference/session-recording/sessions-page.png new file mode 100644 index 000000000..4be14838a Binary files /dev/null and b/docs/images/pam/product-reference/session-recording/sessions-page.png differ diff --git a/docs/images/pam/session-recording/individual-session-page-search.png b/docs/images/pam/session-recording/individual-session-page-search.png deleted file mode 100644 index ce369f515..000000000 Binary files a/docs/images/pam/session-recording/individual-session-page-search.png and /dev/null differ diff --git a/docs/images/pam/session-recording/individual-session-page.png b/docs/images/pam/session-recording/individual-session-page.png deleted file mode 100644 index 2926caf67..000000000 Binary files a/docs/images/pam/session-recording/individual-session-page.png and /dev/null differ diff --git a/docs/images/pam/session-recording/sessions-page-search.png b/docs/images/pam/session-recording/sessions-page-search.png deleted file mode 100644 index a90cda587..000000000 Binary files a/docs/images/pam/session-recording/sessions-page-search.png and /dev/null differ diff --git a/docs/images/pam/session-recording/sessions-page.png b/docs/images/pam/session-recording/sessions-page.png deleted file mode 100644 index 8faab291d..000000000 Binary files a/docs/images/pam/session-recording/sessions-page.png and /dev/null 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/self-hosting/guides/cdn-caching.mdx b/docs/self-hosting/guides/cdn-caching.mdx new file mode 100644 index 000000000..4a6f237f8 --- /dev/null +++ b/docs/self-hosting/guides/cdn-caching.mdx @@ -0,0 +1,106 @@ +--- +title: "CDN Caching for Static Assets" +description: "How to set up CDN caching to prevent version skew issues during deployments" +--- + +This guide explains a common issue with frontend asset caching during deployments and how to solve it using a CDN. + +## The Problem: Version Skew + +Modern frontend build tools like Vite generate content-hashed filenames for static assets (e.g., `main-abc123.js`). Each build produces unique filenames based on file contents. During deployments, this can cause a race condition: + +1. User loads `index.html` which references `main-abc123.js` +2. New deployment replaces containers with a new build +3. New containers only serve `main-xyz789.js` (new build) +4. User's browser requests `main-abc123.js` from cached HTML +5. Request returns **404** — the old asset no longer exists + +This results in broken pages, failed SPA navigation, and requires users to manually refresh. + + +This is a documented limitation in Vite's official guidance: [Load Error Handling](https://vite.dev/guide/build#load-error-handling) + + +### Current Behavior + +Infisical includes a built-in workaround that detects version mismatches and triggers a page reload. While functional, this introduces a noticeable delay for users during deployments. + +## The Solution: External Asset Storage + +The solution is to store static assets externally (e.g., S3, GCS, Azure Blob) and serve them through a CDN (e.g., CloudFront, Cloud CDN, Cloudflare). Assets are uploaded **before** container deployment, ensuring old versions remain available. + +### How It Works + +```mermaid +flowchart LR + User[User Browser] + CDN[CDN] + S3[(Object Storage)] + App[Your Infrastructure] + + User --> CDN + CDN -->|"/assets/*"| S3 + CDN -->|"/* (default)"| App +``` + +The key points: + +- **Asset persistence**: Old assets remain available even after new deployments +- **Deployment order**: Upload new assets before deploying new containers +- **Long cache TTL**: Content-hashed files can be cached indefinitely (we recommend 30 days) +- **Automatic cleanup**: Configure lifecycle rules to expire old assets after 30 days + +At Infisical, we use **CloudFront + S3** for this purpose, but you can use any CDN and object storage combination that fits your infrastructure. + +## Exporting Assets + +Infisical provides a built-in command to export frontend assets from the Docker image: + +```bash +# Export as tar archive to stdout +docker run --rm infisical/infisical npm run --silent assets:export > assets.tar + +# Extract the archive +tar -xf assets.tar +ls assets/ # Content-hashed JS/CSS files +``` + +Or export directly to a mounted directory: + +```bash +docker run --rm -v $(pwd)/cdn-assets:/output \ + infisical/infisical npm run --silent assets:export /output +``` + +### What Gets Exported + +The command exports the `/assets` directory containing: + +- JavaScript bundles (e.g., `main-abc123.js`, `chunk-def456.js`) +- CSS files (e.g., `styles-789xyz.css`) +- Other static assets with content hashes + +These files are safe to cache with long TTLs because their filenames change whenever the content changes. + +## Integration with Your Pipeline + +The general deployment flow should be: + +1. **Build** your new Docker image (or pull the official Infisical image) +2. **Export** assets using `npm run assets:export` +3. **Upload** assets to your object storage +4. **Deploy** the new container version + +```bash +# Example: Export and upload to S3 +docker run --rm infisical/infisical:$VERSION npm run --silent assets:export > assets.tar +tar -xf assets.tar +aws s3 sync assets s3://your-bucket/assets --cache-control "public, max-age=2592000" + +# Then deploy your container +``` + + +Always upload assets **before** deploying the new container. This ensures the assets referenced by the new `index.html` exist before users can access them. + + diff --git a/docs/snippets/AppConnectionsBrowser.jsx b/docs/snippets/AppConnectionsBrowser.jsx index 4bc85efde..c79ef366b 100644 --- a/docs/snippets/AppConnectionsBrowser.jsx +++ b/docs/snippets/AppConnectionsBrowser.jsx @@ -1,71 +1,395 @@ -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"}, - {"name": "MongoDB", "slug": "mongodb", "path": "/integrations/app-connections/mongodb", "description": "Learn how to connect your MongoDB to pull secrets from Infisical.", "category": "Databases"} - ].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", + }, + { + name: "MongoDB", + slug: "mongodb", + path: "/integrations/app-connections/mongodb", + description: "Learn how to connect your MongoDB to pull secrets from Infisical.", + category: "Databases" + } + ].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()) ); } @@ -78,8 +402,18 @@ export const AppConnectionsBrowser = () => {
- - + +
{ {/* Category Filter */}
- {categories.map(category => ( + {categories.map((category) => (
); -}; \ No newline at end of file +}; diff --git a/frontend/index.html b/frontend/index.html index e3a051915..b1dca0a76 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,15 +8,15 @@ http-equiv="Content-Security-Policy" content=" default-src 'self'; - connect-src 'self' https://*.posthog.com http://127.0.0.1:* https://cdn.jsdelivr.net/npm/@lottiefiles/dotlottie-web@0.38.2/dist/dotlottie-player.wasm; - script-src 'self' https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net/npm/@lottiefiles/dotlottie-web@0.38.2/dist/dotlottie-player.wasm; - style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; + connect-src 'self' https://d1zwf0dwl0k2ky.cloudfront.net https://*.posthog.com http://127.0.0.1:* https://cdn.jsdelivr.net/npm/@lottiefiles/dotlottie-web@0.38.2/dist/dotlottie-player.wasm; + script-src 'self' https://d1zwf0dwl0k2ky.cloudfront.net https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net/npm/@lottiefiles/dotlottie-web@0.38.2/dist/dotlottie-player.wasm; + style-src 'self' https://d1zwf0dwl0k2ky.cloudfront.net 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; child-src https://api.stripe.com; frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com; - connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:* https://hcaptcha.com https://*.hcaptcha.com; - img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:; - media-src https://js.intercomcdn.com; - font-src 'self' https://fonts.intercomcdn.com/ https://fonts.gstatic.com; + connect-src 'self' https://d1zwf0dwl0k2ky.cloudfront.net wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:* https://hcaptcha.com https://*.hcaptcha.com; + img-src 'self' https://d1zwf0dwl0k2ky.cloudfront.net https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:; + media-src https://d1zwf0dwl0k2ky.cloudfront.net https://js.intercomcdn.com; + font-src 'self' https://d1zwf0dwl0k2ky.cloudfront.net https://fonts.intercomcdn.com/ https://fonts.gstatic.com; " /> Infisical 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/auth/TeamInviteStep.tsx b/frontend/src/components/auth/TeamInviteStep.tsx index ccb1e6f57..c03229a33 100644 --- a/frontend/src/components/auth/TeamInviteStep.tsx +++ b/frontend/src/components/auth/TeamInviteStep.tsx @@ -20,9 +20,14 @@ export default function TeamInviteStep(): JSX.Element { const { mutateAsync } = useAddUsersToOrg(); const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const); + const orgId = String(localStorage.getItem("orgData.id")); + // Redirect user to the getting started page const redirectToHome = async () => { - navigate({ to: "/organization/projects" as const }); + navigate({ + to: orgId ? ("/organizations/$orgId/projects" as const) : "/", + params: { orgId } + }); }; const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => { @@ -32,7 +37,7 @@ export default function TeamInviteStep(): JSX.Element { .map(async (email) => { mutateAsync({ inviteeEmails: [email], - organizationId: String(localStorage.getItem("orgData.id")), + organizationId: orgId, organizationRoleSlug: "member" }); }); diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index a912980f1..127b1066e 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -70,7 +70,8 @@ export default function NavHeader({ {currentOrg?.name?.charAt(0)}
{currentOrg?.name} @@ -90,8 +91,8 @@ export default function NavHeader({ {pageName === "Secrets" ? ( {pageName} @@ -126,8 +127,8 @@ export default function NavHeader({
{userAvailableEnvs?.find(({ slug }) => slug === currentEnv)?.name} @@ -188,8 +189,9 @@ export default function NavHeader({
) : ( { + const { currentOrg } = useOrganization(); const [, isCopying, setIsCopying] = useTimedReset({ initialState: false }); @@ -69,8 +71,9 @@ export const SecretDashboardPathBreadcrumb = ({
) : ( = ({ isOpen, onClose }) => }); navigate({ - to: "/organization/projects" + to: "/organizations/$orgId/projects", + params: { orgId: organization.id } }); localStorage.setItem("orgData.id", organization.id); 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/projects/NewProjectModal.tsx b/frontend/src/components/projects/NewProjectModal.tsx index 663d6a0b7..d9887a929 100644 --- a/frontend/src/components/projects/NewProjectModal.tsx +++ b/frontend/src/components/projects/NewProjectModal.tsx @@ -157,7 +157,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { onOpenChange(false); navigate({ to: getProjectHomePage(project.type, project.environments), - params: { projectId: project.id } + params: { projectId: project.id, orgId: currentOrg.id } }); }; const onSubmit = handleSubmit((data) => { 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 = ({ }) => ( + - Documentation + ); diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 5f199443d..6046c8fc8 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -25,343 +25,343 @@ export const ROUTE_PATHS = Object.freeze({ Organization: { Settings: { OauthCallbackPage: setRoute( - "/organization/settings/oauth/callback", - "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback" + "/organizations/$orgId/settings/oauth/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/settings/oauth/callback" ) }, SecretSharing: setRoute( - "/organization/secret-sharing", - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" + "/organizations/$orgId/secret-sharing", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/secret-sharing/" ), SettingsPage: setRoute( - "/organization/settings", - "/_authenticate/_inject-org-details/_org-layout/organization/settings/" + "/organizations/$orgId/settings", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/settings/" ), GroupDetailsByIDPage: setRoute( - "/organization/groups/$groupId", - "/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId" + "/organizations/$orgId/groups/$groupId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/groups/$groupId" ), IdentityDetailsByIDPage: setRoute( - "/organization/identities/$identityId", - "/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId" + "/organizations/$orgId/identities/$identityId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/identities/$identityId" ), UserDetailsByIDPage: setRoute( - "/organization/members/$membershipId", - "/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId" + "/organizations/$orgId/members/$membershipId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/members/$membershipId" ), AccessControlPage: setRoute( - "/organization/access-management", - "/_authenticate/_inject-org-details/_org-layout/organization/access-management" + "/organizations/$orgId/access-management", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/access-management" ), RoleByIDPage: setRoute( - "/organization/roles/$roleId", - "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId" + "/organizations/$orgId/roles/$roleId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/roles/$roleId" ), AppConnections: { OauthCallbackPage: setRoute( - "/organization/app-connections/$appConnection/oauth/callback", - "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" + "/organizations/$orgId/app-connections/$appConnection/oauth/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/app-connections/$appConnection/oauth/callback" ) }, NetworkingPage: setRoute( - "/organization/networking", - "/_authenticate/_inject-org-details/_org-layout/organization/networking" + "/organizations/$orgId/networking", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/networking" ) }, SecretManager: { ApprovalPage: setRoute( - "/projects/secret-management/$projectId/approval", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval" + "/organizations/$orgId/projects/secret-management/$projectId/approval", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/approval" ), SecretDashboardPage: setRoute( - "/projects/secret-management/$projectId/secrets/$envSlug", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug" + "/organizations/$orgId/projects/secret-management/$projectId/secrets/$envSlug", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug" ), RollbackPreviewPage: setRoute( - "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore" + "/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore" ), CommitDetailsPage: setRoute( - "/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId" + "/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId" ), CommitsPage: setRoute( - "/projects/secret-management/$projectId/commits/$environment/$folderId", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId" + "/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId" ), OverviewPage: setRoute( - "/projects/secret-management/$projectId/overview", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview" + "/organizations/$orgId/projects/secret-management/$projectId/overview", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/overview" ), IntegrationsListPage: setRoute( - "/projects/secret-management/$projectId/integrations", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/" + "/organizations/$orgId/projects/secret-management/$projectId/integrations", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/" ), IntegrationDetailsByIDPage: setRoute( - "/projects/secret-management/$projectId/integrations/$integrationId", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/$integrationId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/$integrationId" ), SecretSyncDetailsByIDPage: setRoute( - "/projects/secret-management/$projectId/integrations/secret-syncs/$destination/$syncId", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/secret-syncs/$destination/$syncId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId" ), Integratons: { SelectIntegrationAuth: setRoute( - "/projects/secret-management/$projectId/integrations/select-integration-auth", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/select-integration-auth", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/select-integration-auth" ), HerokuOauthCallbackPage: setRoute( - "/projects/secret-management/$projectId/integrations/heroku/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/heroku/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/oauth2/callback" ), HerokuConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/heroku/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/heroku/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/heroku/create" ), AwsParameterStoreConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/aws-parameter-store/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/aws-parameter-store/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-parameter-store/create" ), AwsSecretManagerConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/aws-secret-manager/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/aws-secret-manager/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/aws-secret-manager/create" ), AzureAppConfigurationsOauthCallbackPage: setRoute( - "/projects/secret-management/$projectId/integrations/azure-app-configuration/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-app-configuration/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/oauth2/callback" ), AzureAppConfigurationsConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/azure-app-configuration/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-app-configuration/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-app-configuration/create" ), AzureDevopsConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/azure-devops/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-devops/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-devops/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-devops/create" ), AzureKeyVaultAuthorizePage: setRoute( - "/projects/secret-management/$projectId/integrations/azure-key-vault/authorize", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-key-vault/authorize", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/authorize" ), AzureKeyVaultOauthCallbackPage: setRoute( - "/projects/secret-management/$projectId/integrations/azure-key-vault/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-key-vault/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/oauth2/callback" ), AzureKeyVaultConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/azure-key-vault/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/azure-key-vault/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/azure-key-vault/create" ), BitbucketOauthCallbackPage: setRoute( - "/projects/secret-management/$projectId/integrations/bitbucket/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/bitbucket/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/oauth2/callback" ), BitbucketConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/bitbucket/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/bitbucket/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/bitbucket/create" ), ChecklyConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/checkly/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/checkly/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/checkly/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/checkly/create" ), CircleConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/circleci/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/circleci/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/circleci/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/circleci/create" ), CloudflarePagesConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/cloudflare-pages/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/cloudflare-pages/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-pages/create" ), DigitalOceanAppPlatformConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/digital-ocean-app-platform/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/digital-ocean-app-platform/create" ), CloudflareWorkersConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/cloudflare-workers/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/cloudflare-workers/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloudflare-workers/create" ), CodefreshConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/codefresh/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/codefresh/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/codefresh/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/codefresh/create" ), GcpSecretManagerConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/gcp-secret-manager/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/gcp-secret-manager/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/create" ), GcpSecretManagerOauthCallbackPage: setRoute( - "/projects/secret-management/$projectId/integrations/gcp-secret-manager/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/gcp-secret-manager/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/gcp-secret-manager/oauth2/callback" ), GithubConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/github/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/github/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/create" ), GithubOauthCallbackPage: setRoute( - "/projects/secret-management/$projectId/integrations/github/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/oauth2/callback" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/github/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/github/oauth2/callback" ), GitlabConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/gitlab/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/gitlab/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/create" ), GitlabOauthCallbackPage: setRoute( - "/projects/secret-management/$projectId/integrations/gitlab/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/gitlab/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/gitlab/oauth2/callback" ), VercelOauthCallbackPage: setRoute( - "/projects/secret-management/$projectId/integrations/vercel/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/vercel/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback" ), VercelConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/vercel/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/vercel/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/create" ), FlyioConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/flyio/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/flyio/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/flyio/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/flyio/create" ), HashicorpVaultConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/hashicorp-vault/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/hashicorp-vault/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/hashicorp-vault/create" ), HasuraCloudConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/hasura-cloud/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/hasura-cloud/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/hasura-cloud/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/hasura-cloud/create" ), LaravelForgeConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/laravel-forge/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/laravel-forge/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/laravel-forge/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/laravel-forge/create" ), NorthflankConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/northflank/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/northflank/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/northflank/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/northflank/create" ), RailwayConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/railway/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/railway/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/railway/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/railway/create" ), RenderConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/render/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/render/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/render/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/render/create" ), RundeckConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/rundeck/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/rundeck/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/rundeck/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/rundeck/create" ), WindmillConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/windmill/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/windmill/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/windmill/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/windmill/create" ), TravisCIConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/travisci/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/travisci/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/travisci/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/travisci/create" ), TerraformCloudConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/terraform-cloud/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/terraform-cloud/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/terraform-cloud/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/terraform-cloud/create" ), TeamcityConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/teamcity/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/teamcity/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/teamcity/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/teamcity/create" ), SupabaseConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/supabase/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/supabase/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/supabase/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/supabase/create" ), OctopusDeployCloudConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/octopus-deploy/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/octopus-deploy/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/octopus-deploy/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/octopus-deploy/create" ), DatabricksConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/databricks/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/databricks/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/databricks/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/databricks/create" ), QoveryConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/qovery/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/qovery/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/qovery/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/qovery/create" ), Cloud66ConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/cloud-66/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloud-66/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/cloud-66/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/cloud-66/create" ), NetlifyConfigurePage: setRoute( - "/projects/secret-management/$projectId/integrations/netlify/create", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/create" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/netlify/create", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/create" ), NetlifyOuathCallbackPage: setRoute( - "/projects/secret-management/$projectId/integrations/netlify/oauth2/callback", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback" + "/organizations/$orgId/projects/secret-management/$projectId/integrations/netlify/oauth2/callback", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/netlify/oauth2/callback" ) } }, CertManager: { CertAuthDetailsByIDPage: setRoute( - "/projects/cert-management/$projectId/ca/$caName", - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/ca/$caName" + "/organizations/$orgId/projects/cert-management/$projectId/ca/$caId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId" ), SubscribersPage: setRoute( - "/projects/cert-management/$projectId/subscribers", - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers" + "/organizations/$orgId/projects/cert-management/$projectId/subscribers", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers" ), CertificateAuthoritiesPage: setRoute( - "/projects/cert-management/$projectId/certificate-authorities", - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities" + "/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities" ), AlertingPage: setRoute( - "/projects/cert-management/$projectId/alerting", - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting" + "/organizations/$orgId/projects/cert-management/$projectId/alerting", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/alerting" ), PkiCollectionDetailsByIDPage: setRoute( - "/projects/cert-management/$projectId/pki-collections/$collectionId", - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/pki-collections/$collectionId" + "/organizations/$orgId/projects/cert-management/$projectId/pki-collections/$collectionId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/pki-collections/$collectionId" ), PkiSubscriberDetailsByIDPage: setRoute( - "/projects/cert-management/$projectId/subscribers/$subscriberName", - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName" + "/organizations/$orgId/projects/cert-management/$projectId/subscribers/$subscriberName", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName" ), IntegrationsListPage: setRoute( - "/projects/cert-management/$projectId/integrations", - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/" + "/organizations/$orgId/projects/cert-management/$projectId/integrations", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/" ), PkiSyncDetailsByIDPage: setRoute( - "/projects/cert-management/$projectId/integrations/$syncId", - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId" + "/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId" ) }, Ssh: { SshCaByIDPage: setRoute( - "/projects/ssh/$projectId/ca/$caId", - "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ca/$caId" + "/organizations/$orgId/projects/ssh/$projectId/ca/$caId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ssh/$projectId/_ssh-layout/ca/$caId" ), SshHostGroupDetailsByIDPage: setRoute( - "/projects/ssh/$projectId/ssh-host-groups/$sshHostGroupId", - "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId" + "/organizations/$orgId/projects/ssh/$projectId/ssh-host-groups/$sshHostGroupId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId" ) }, SecretScanning: { DataSourceByIdPage: setRoute( - "/projects/secret-scanning/$projectId/data-sources/$type/$dataSourceId", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId" + "/organizations/$orgId/projects/secret-scanning/$projectId/data-sources/$type/$dataSourceId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/data-sources/$type/$dataSourceId" ), FindingsPage: setRoute( - "/projects/secret-scanning/$projectId/findings", - "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/findings" + "/organizations/$orgId/projects/secret-scanning/$projectId/findings", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/findings" ) }, Pam: { AccountsPage: setRoute( - "/projects/pam/$projectId/accounts", - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts" + "/organizations/$orgId/projects/pam/$projectId/accounts", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/accounts" ), ResourcesPage: setRoute( - "/projects/pam/$projectId/resources", - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources" + "/organizations/$orgId/projects/pam/$projectId/resources", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/resources" ), SessionsPage: setRoute( - "/projects/pam/$projectId/sessions", - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/" + "/organizations/$orgId/projects/pam/$projectId/sessions", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/" ), PamSessionByIDPage: setRoute( - "/projects/pam/$projectId/sessions/$sessionId", - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/$sessionId" + "/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId" ) }, Public: { diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 3499c5bc8..e8857e022 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -50,6 +50,7 @@ import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/b import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection"; import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection"; import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean"; +import { DNSMadeEasyConnectionMethod } from "@app/hooks/api/appConnections/types/dns-made-easy-connection"; import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection"; import { NetlifyConnectionMethod } from "@app/hooks/api/appConnections/types/netlify-connection"; @@ -112,6 +113,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, [AppConnection.GitLab]: { name: "GitLab", image: "GitLab.png" }, [AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" }, + [AppConnection.DNSMadeEasy]: { name: "DNS Made Easy", image: "DNSMadeEasy.svg", size: 120 }, [AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" }, [AppConnection.Railway]: { name: "Railway", image: "Railway.png" }, [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }, @@ -217,6 +219,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "Client Secret", icon: faKey }; case AzureClientSecretsConnectionMethod.Certificate: return { name: "Certificate", icon: faCertificate }; + case DNSMadeEasyConnectionMethod.APIKeySecret: + return { name: "API Key & Secret", icon: faKey }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index 11d561e02..128dd31ce 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -14,11 +14,6 @@ const secretsToBeAdded = [ secretValue: "OVERRIDE_THIS", secretComment: "Override secrets with personal value" }, - { - secretKey: "DB_PASSWORD", - secretValue: "OVERRIDE_THIS", - secretComment: "Another secret override" - }, { secretKey: "DB_PASSWORD", secretValue: "example_password" @@ -64,11 +59,11 @@ export const initProjectHelper = async ({ projectName }: { projectName: string } export const getProjectBaseURL = (type: ProjectType) => { switch (type) { case ProjectType.SecretManager: - return "/projects/secret-management/$projectId"; + return "/organizations/$orgId/projects/secret-management/$projectId"; case ProjectType.CertificateManager: - return "/projects/cert-management/$projectId"; + return "/organizations/$orgId/projects/cert-management/$projectId"; default: - return `/projects/${type}/$projectId` as const; + return `/organizations/$orgId/projects/${type}/$projectId` as const; } }; @@ -77,15 +72,15 @@ export const getProjectBaseURL = (type: ProjectType) => { export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[]) => { switch (type) { case ProjectType.SecretManager: - return "/projects/secret-management/$projectId/overview" as const; + return "/organizations/$orgId/projects/secret-management/$projectId/overview" as const; case ProjectType.CertificateManager: - return "/projects/cert-management/$projectId/policies" as const; + return "/organizations/$orgId/projects/cert-management/$projectId/policies" as const; case ProjectType.SecretScanning: - return `/projects/${type}/$projectId/data-sources` as const; + return `/organizations/$orgId/projects/${type}/$projectId/data-sources` as const; case ProjectType.PAM: - return `/projects/${type}/$projectId/accounts` as const; + return `/organizations/$orgId/projects/${type}/$projectId/accounts` as const; default: - return `/projects/${type}/$projectId/overview` as const; + return `/organizations/$orgId/projects/${type}/$projectId/overview` as const; } }; diff --git a/frontend/src/hooks/api/appConnections/dns-made-easy/index.ts b/frontend/src/hooks/api/appConnections/dns-made-easy/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/dns-made-easy/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/dns-made-easy/queries.tsx b/frontend/src/hooks/api/appConnections/dns-made-easy/queries.tsx new file mode 100644 index 000000000..6a5df54ac --- /dev/null +++ b/frontend/src/hooks/api/appConnections/dns-made-easy/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TDNSMadeEasyZone } from "./types"; + +const dnsMadeEasyConnectionKeys = { + all: [...appConnectionKeys.all, "dns-made-easy"] as const, + listZones: (connectionId: string) => + [...dnsMadeEasyConnectionKeys.all, "zones", connectionId] as const +}; + +export const useDNSMadeEasyConnectionListZones = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TDNSMadeEasyZone[], + unknown, + TDNSMadeEasyZone[], + ReturnType + >, + "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 d04b42d4c..dbe6c7367 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 dd88f86ef..9c0f78a0c 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -188,6 +188,10 @@ export type TMongoDBConnectionOption = TAppConnectionOptionBase & { app: AppConnection.MongoDB; }; +export type TDNSMadeEasyConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.DNSMadeEasy; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -231,7 +235,8 @@ export type TAppConnectionOption = | TLaravelForgeConnectionOption | TRedisConnectionOption | TMongoDBConnectionOption - | TChefConnectionOption; + | TChefConnectionOption + | TDNSMadeEasyConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -263,6 +268,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 8fd089680..c78d2aed2 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"; @@ -58,6 +59,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"; @@ -130,7 +132,8 @@ export type TAppConnection = | TOktaConnection | TRedisConnection | TMongoDBConnection - | 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..d2e1836bb 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), @@ -192,19 +189,21 @@ export const useGetCaCertTemplates = (caId: string) => { export const useGetAzureAdcsTemplates = ({ caId, - projectId + projectId, + isAzureAdcsCa }: { caId: string; projectId: string; + isAzureAdcsCa: boolean; }) => { return useQuery({ queryKey: caKeys.getAzureAdcsTemplates(caId, projectId), 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) + enabled: Boolean(caId && projectId && isAzureAdcsCa) }); }; 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/index.ts b/frontend/src/hooks/api/certificateProfiles/index.ts index e12e066c4..27e73c15a 100644 --- a/frontend/src/hooks/api/certificateProfiles/index.ts +++ b/frontend/src/hooks/api/certificateProfiles/index.ts @@ -10,4 +10,4 @@ export { useGetProfileCertificates, useListCertificateProfiles } from "./queries"; -export type * from "./types"; +export * from "./types"; 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/certificateProfiles/types.ts b/frontend/src/hooks/api/certificateProfiles/types.ts index c2b38e8e8..a4f623659 100644 --- a/frontend/src/hooks/api/certificateProfiles/types.ts +++ b/frontend/src/hooks/api/certificateProfiles/types.ts @@ -1,23 +1,46 @@ +export enum EnrollmentType { + API = "api", + EST = "est", + ACME = "acme" +} + +export enum IssuerType { + CA = "ca", + SELF_SIGNED = "self-signed" +} + export type TCertificateProfile = { id: string; projectId: string; - caId: string; + caId: string | null; certificateTemplateId: string; slug: string; description?: string; - enrollmentType: "api" | "est" | "acme"; + enrollmentType: EnrollmentType; + issuerType: IssuerType; estConfigId?: string; apiConfigId?: string; createdAt: string; updatedAt: string; + externalConfigs?: Record | null; + certificateAuthority?: { + id: string; + projectId?: string; + status: string; + name: string; + isExternal?: boolean; + externalType?: string | null; + }; }; export type TCertificateProfileWithDetails = TCertificateProfile & { certificateAuthority?: { id: string; - projectId: string; + projectId?: string; status: string; name: string; + isExternal?: boolean; + externalType?: string | null; }; certificateTemplate?: { id: string; @@ -44,11 +67,12 @@ export type TCertificateProfileWithDetails = TCertificateProfile & { export type TCreateCertificateProfileDTO = { projectId: string; - caId: string; + caId?: string; certificateTemplateId: string; slug: string; description?: string; - enrollmentType: "api" | "est" | "acme"; + enrollmentType: EnrollmentType; + issuerType: IssuerType; estConfig?: { disableBootstrapCaValidation?: boolean; passphrase: string; @@ -59,12 +83,15 @@ export type TCreateCertificateProfileDTO = { renewBeforeDays?: number; }; acmeConfig?: unknown; + externalConfigs?: Record | null; }; export type TUpdateCertificateProfileDTO = { profileId: string; slug?: string; description?: string; + enrollmentType?: EnrollmentType; + issuerType?: IssuerType; estConfig?: { disableBootstrapCaValidation?: boolean; passphrase?: string; @@ -75,6 +102,7 @@ export type TUpdateCertificateProfileDTO = { renewBeforeDays?: number; }; acmeConfig?: unknown; + externalConfigs?: Record | null; }; export type TDeleteCertificateProfileDTO = { @@ -87,7 +115,9 @@ export type TListCertificateProfilesDTO = { offset?: number; search?: string; includeConfigs?: boolean; - enrollmentType?: "api" | "est" | "acme"; + enrollmentType?: EnrollmentType; + issuerType?: IssuerType; + caId?: string; }; export type TGetCertificateProfileByIdDTO = { 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 f65d6adc6..03605ac90 100644 --- a/frontend/src/hooks/api/certificates/mutations.tsx +++ b/frontend/src/hooks/api/certificates/mutations.tsx @@ -13,17 +13,19 @@ import { TRenewCertificateDTO, TRenewCertificateResponse, TRevokeCertDTO, + TUnifiedCertificateIssuanceDTO, + TUnifiedCertificateIssuanceResponse, TUpdateRenewalConfigDTO } from "./types"; 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; }, @@ -47,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 } @@ -80,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; @@ -98,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; @@ -131,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; @@ -149,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 @@ -168,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); @@ -185,3 +187,31 @@ export const useDownloadCertPkcs12 = () => { } }); }; + +export const useUnifiedCertificateIssuance = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { projectSlug, ...requestData } = body; + const { data } = await apiRequest.post( + "/api/v1/cert-manager/certificates", + requestData + ); + return data; + }, + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries({ + queryKey: ["certificate-profiles", "list"] + }); + queryClient.invalidateQueries({ + queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates() + }); + queryClient.invalidateQueries({ + queryKey: projectKeys.allProjectCertificates() + }); + queryClient.invalidateQueries({ + queryKey: projectKeys.forProjectCertificates(projectSlug) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/certificates/queries.tsx b/frontend/src/hooks/api/certificates/queries.tsx index 50f2836ed..13245894b 100644 --- a/frontend/src/hooks/api/certificates/queries.tsx +++ b/frontend/src/hooks/api/certificates/queries.tsx @@ -7,7 +7,11 @@ import { TCertificate } from "./types"; export const certKeys = { getCertById: (serialNumber: string) => [{ serialNumber }, "cert"], getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"], - getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"] + getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"], + getCertificateRequest: (requestId: string, projectSlug: string) => [ + { requestId, projectSlug }, + "certificateRequest" + ] }; export const useGetCert = (serialNumber: string) => { diff --git a/frontend/src/hooks/api/certificates/types.ts b/frontend/src/hooks/api/certificates/types.ts index ee543aac7..4d34822b3 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; }; @@ -64,6 +64,7 @@ export type TRenewCertificateResponse = { serialNumber: string; certificateId: string; projectId: string; + certificateRequestId?: string; }; export type TUpdateRenewalConfigDTO = { @@ -74,8 +75,64 @@ export type TUpdateRenewalConfigDTO = { }; export type TDownloadPkcs12DTO = { - serialNumber: string; + certificateId: string; projectSlug: string; password: string; alias: string; }; + +export type TUnifiedCertificateIssuanceDTO = { + projectSlug: string; + profileId: string; + projectId: string; + csr?: string; + attributes?: { + commonName?: string; + keyUsages?: string[]; + extendedKeyUsages?: string[]; + altNames?: Array<{ + type: string; + value: string; + }>; + signatureAlgorithm: string; + keyAlgorithm: string; + subjectAlternativeNames?: Array<{ + type: string; + value: string; + }>; + ttl: string; + notBefore?: string; + notAfter?: string; + }; + removeRootsFromChain?: boolean; +}; + +export type TUnifiedCertificateResponse = { + certificate: { + certificate: string; + issuingCaCertificate: string; + certificateChain: string; + privateKey?: string; + serialNumber: string; + certificateId: string; + }; + certificateRequestId: string; +}; + +export type TCertificateRequestResponse = { + certificateRequestId: string; + status: "pending" | "issued" | "failed"; + projectId: string; +}; + +export type TUnifiedCertificateIssuanceResponse = + | TUnifiedCertificateResponse + | TCertificateRequestResponse; + +export type TCertificateRequestDetails = { + status: "pending" | "issued" | "failed"; + certificate: TCertificate | null; + errorMessage: string | null; + createdAt: string; + updatedAt: 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/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 7777712ae..88c52ed05 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -338,7 +338,7 @@ export const clearSession = (keepQueryClient?: boolean) => { sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN); if (!keepQueryClient) { - qc.clear(); // Clear React Query cache + qc.invalidateQueries(); } }; diff --git a/frontend/src/index.css b/frontend/src/index.css index 2a1da3fa2..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"; /* @@ -29,11 +25,11 @@ } :root { - font-family: var(--font-inter); - --foreground: oklch(0.985 0 0); - --background: oklch(0.145 0 0); + font-family: var(--font-inter); + --foreground: oklch(0.985 0 0); + --background: oklch(0.145 0 0); - --toastify-color-dark: var(--color-mineshaft-700); + --toastify-color-dark: var(--color-mineshaft-700); } @theme { @@ -45,16 +41,16 @@ --color-background: #19191c; --color-foreground: white; --color-success: #2ecc71; - --color-info: #34c2db; + --color-info: #63b0bd; --color-warning: #f1c40f; --color-danger: #e74c3c; - --color-org: #30B3FF; + --color-org: #30b3ff; --color-sub-org: #96ff59; --color-project: #e0ed34; --color-neutral: #adaeb0; /*legacy color schema */ - --color-org-v1: #30B3FF; + --color-org-v1: #30b3ff; --color-namespace-v1: #96ff59; /* Primary */ @@ -418,15 +414,15 @@ } .Toastify__toast { - @apply rounded-md; + @apply rounded-md; } .Toastify__toast-body { - @apply items-start; + @apply items-start; } .Toastify__toast-icon { - @apply w-4 pt-1; + @apply w-4 pt-1; } .tags-conic-bg { diff --git a/frontend/src/layouts/AdminLayout/AdminNavBar.tsx b/frontend/src/layouts/AdminLayout/AdminNavBar.tsx index 49cc1edf9..791a15a10 100644 --- a/frontend/src/layouts/AdminLayout/AdminNavBar.tsx +++ b/frontend/src/layouts/AdminLayout/AdminNavBar.tsx @@ -14,6 +14,7 @@ import { Link, useMatchRoute } from "@tanstack/react-router"; import { motion } from "framer-motion"; import { Tab, TabList, Tabs, Tooltip } from "@app/components/v2"; +import { useOrganization } from "@app/context"; const generalTabs = [ { @@ -60,6 +61,7 @@ const generalTabs = [ export const AdminNavBar = () => { const matchRoute = useMatchRoute(); + const { currentOrg } = useOrganization(); return (
@@ -74,7 +76,7 @@ export const AdminNavBar = () => { - + diff --git a/frontend/src/layouts/KmsLayout/KmsLayout.tsx b/frontend/src/layouts/KmsLayout/KmsLayout.tsx index eef78000a..8ffe0da44 100644 --- a/frontend/src/layouts/KmsLayout/KmsLayout.tsx +++ b/frontend/src/layouts/KmsLayout/KmsLayout.tsx @@ -2,19 +2,20 @@ import { Link, Outlet, useLocation } from "@tanstack/react-router"; import { motion } from "framer-motion"; import { Tab, TabList, Tabs } from "@app/components/v2"; -import { useProject, useProjectPermission } from "@app/context"; +import { useOrganization, useProject, useProjectPermission } from "@app/context"; import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner"; export const KmsLayout = () => { const { currentProject } = useProject(); + const { currentOrg } = useOrganization(); const { assumedPrivilegeDetails } = useProjectPermission(); const location = useLocation(); return ( -
-
+
+
{ {({ isActive }) => Overview} {({ isActive }) => KMIP} @@ -62,16 +66,18 @@ export const KmsLayout = () => { )} {({ isActive }) => Audit Logs} diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 6be963e2b..4610546cb 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -22,8 +22,8 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; -import { UserPlusIcon } from "lucide-react"; +import { Link, useLocation, useNavigate, useRouter } from "@tanstack/react-router"; +import { ChevronRight, UserPlusIcon } from "lucide-react"; import { twMerge } from "tailwind-merge"; import { Mfa } from "@app/components/auth/Mfa"; @@ -31,7 +31,6 @@ import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { - BreadcrumbContainer, Button, DropdownMenu, DropdownMenuContent, @@ -43,7 +42,6 @@ import { IconButton, Modal, ModalContent, - TBreadcrumbFormat, Tooltip } from "@app/components/v2"; import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3"; @@ -69,6 +67,7 @@ import { MfaMethod } from "@app/hooks/api/auth/types"; import { getAuthToken } from "@app/hooks/api/reactQuery"; import { Organization, SubscriptionPlan } from "@app/hooks/api/types"; import { AuthMethod } from "@app/hooks/api/users/types"; +import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; @@ -183,9 +182,6 @@ export const Navbar = () => { } }, [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 isOrgScope = location.pathname.startsWith("/organization"); // TODO: scott/akhil is this adequate? + 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 && ( { { navigate({ - to: "/organization/projects", - search: (prev) => ({ ...prev, subOrganization: subOrg.name }) + to: "/organizations/$orgId/projects", + params: { orgId: subOrg.id } }); await router.invalidate({ sync: true }).catch(() => null); }} @@ -477,11 +470,11 @@ export const Navbar = () => { // TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar className={twMerge( "gap-x-1.5 text-sm", - !isOrgScope && + isProjectScope && "min-w-6 bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org" )} > - + {currentOrg.subOrganization.name} @@ -511,8 +504,8 @@ export const Navbar = () => { { navigate({ - to: "/organization/projects", - search: (prev) => ({ ...prev, subOrganization: subOrg.name }) + to: "/organizations/$orgId/projects", + params: { orgId: subOrg.id } }); await router.invalidate({ sync: true }).catch(() => null); }} @@ -541,21 +534,17 @@ export const Navbar = () => { )} - {!isOrgScope && ( + {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") ? ( @@ -593,14 +586,15 @@ export const Navbar = () => { isAllowed ? ( - Invite Members + Invite Users ) : null } @@ -696,14 +690,15 @@ export const Navbar = () => { {(isAllowed) => isAllowed ? ( }> - Invite Members + Invite Users ) : null @@ -778,7 +773,11 @@ export const Navbar = () => {
- + diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx index 49ffe3494..fff941681 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx @@ -20,9 +20,9 @@ import { } from "@app/components/v2"; import { useProject } from "@app/context"; import { useGetCert } from "@app/hooks/api"; -import { useCreateCertificateV3 } from "@app/hooks/api/ca"; -import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles"; +import { EnrollmentType, useListCertificateProfiles } from "@app/hooks/api/certificateProfiles"; import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums"; +import { useUnifiedCertificateIssuance } from "@app/hooks/api/certificates/mutations"; import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertSubjectAlternativeNameType } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants"; @@ -103,10 +103,11 @@ type Props = { }; type TCertificateDetails = { - serialNumber: string; - certificate: string; - certificateChain: string; - privateKey: string; + serialNumber?: string; + certificate?: string; + certificateChain?: string; + privateKey?: string; + issuingCaCertificate?: string; }; export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => { @@ -122,12 +123,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } const { data: profilesData } = useListCertificateProfiles({ projectId: currentProject?.id || "", - enrollmentType: "api" + enrollmentType: EnrollmentType.API, + includeConfigs: true }); - const { mutateAsync: createCertificate } = useCreateCertificateV3({ - projectId: currentProject?.id - }); + const { mutateAsync: issueCertificate } = useUnifiedCertificateIssuance(); const formResolver = useMemo(() => { return zodResolver(createSchema(shouldShowSubjectSection)); @@ -243,7 +243,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } keyUsages, extendedKeyUsages }: FormData) => { - if (!currentProject?.slug) { + if (!currentProject?.slug || !currentProject?.id) { createNotification({ text: "Project not found. Please refresh and try again.", type: "error" @@ -275,44 +275,70 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } } } - const certificateRequest: any = { - profileId: formProfileId, - projectSlug: currentProject.slug, - ttl, - signatureAlgorithm, - keyAlgorithm, - keyUsages: filterUsages(keyUsages) as CertKeyUsage[], - extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[] - }; + try { + // Prepare unified request + const request: any = { + profileId: formProfileId, + projectSlug: currentProject.slug, + projectId: currentProject.id, + attributes: { + ttl, + signatureAlgorithm: signatureAlgorithm || "", + keyAlgorithm: keyAlgorithm || "", + keyUsages: filterUsages(keyUsages) as CertKeyUsage[], + extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[] + } + }; - if (constraints.shouldShowSubjectSection && commonName) { - certificateRequest.commonName = commonName; - } - if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) { - const formattedSans = formatSubjectAltNames(subjectAltNames); - if (formattedSans && formattedSans.length > 0) { - certificateRequest.altNames = formattedSans; + if (constraints.shouldShowSubjectSection && commonName) { + request.attributes.commonName = commonName; } + + if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) { + const formattedSans = formatSubjectAltNames(subjectAltNames); + if (formattedSans && formattedSans.length > 0) { + request.attributes.altNames = formattedSans; + } + } + + const response = await issueCertificate(request); + + // Handle certificate issuance response + + if ("certificate" in response && response.certificate) { + const certData = response.certificate; + const certificateDetailsToSet = { + serialNumber: certData.serialNumber || "", + certificate: certData.certificate || "", + certificateChain: certData.certificateChain || "", + privateKey: certData.privateKey || "", + issuingCaCertificate: certData.issuingCaCertificate || "" + }; + + setCertificateDetails(certificateDetailsToSet); + + createNotification({ + text: "Successfully created certificate", + type: "success" + }); + } else { + // Certificate request - async processing + createNotification({ + text: `Certificate request submitted successfully. This may take a few minutes to process. Certificate Request ID: ${response.certificateRequestId}`, + type: "success" + }); + handlePopUpToggle("issueCertificate", false); + } + } catch (error) { + createNotification({ + text: `Failed to request certificate: ${(error as Error)?.message || "Unknown error"}`, + type: "error" + }); } - - const { serialNumber, certificate, certificateChain, privateKey } = - await createCertificate(certificateRequest); - - setCertificateDetails({ - serialNumber, - certificate, - certificateChain, - privateKey - }); - - createNotification({ - text: "Successfully created certificate", - type: "success" - }); }, [ currentProject?.slug, - createCertificate, + issueCertificate, constraints.shouldShowSubjectSection, constraints.shouldShowSanSection ] @@ -321,13 +347,13 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } const getModalTitle = () => { if (certificateDetails) return "Certificate Created Successfully"; if (cert) return "Certificate Details"; - return "Issue New Certificate"; + return "Request New Certificate"; }; const getModalSubTitle = () => { if (certificateDetails) return "Certificate has been successfully created and is ready for use"; if (cert) return "View certificate information"; - return "Issue a new certificate using a certificate profile"; + return "Request a new certificate using a certificate profile"; }; return ( @@ -343,10 +369,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } {certificateDetails && ( )} {cert && ( @@ -498,7 +524,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } isLoading={isSubmitting} isDisabled={isSubmitting || (!actualSelectedProfile && !profileId)} > - {cert ? "Update" : "Issue Certificate"} + {cert ? "Update" : "Request Certificate"}
)} @@ -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 a60142762..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 }) } @@ -416,7 +417,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { {/* Manual renewal action for profile-issued certificates that are not revoked/expired (including failed ones) */} {(() => { const canRenew = - certificate.profileId && + (certificate.profileId || certificate.caId) && certificate.hasPrivateKey !== false && !certificate.renewedByCertificateId && !isRevoked && @@ -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 8358c28a3..8cf23c725 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/IntegrationsListPage.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/IntegrationsListPage.tsx @@ -5,7 +5,7 @@ import { useNavigate, useSearch } from "@tanstack/react-router"; import { ProjectPermissionCan } from "@app/components/permissions"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionSub, useProject } from "@app/context"; +import { ProjectPermissionSub, useOrganization, useProject } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectType } from "@app/hooks/api/projects/types"; import { IntegrationsListPageTabs } from "@app/types/integrations"; @@ -14,6 +14,7 @@ import { PkiSyncsTab } from "./components"; export const IntegrationsListPage = () => { const navigate = useNavigate(); + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const { t } = useTranslation(); @@ -30,7 +31,8 @@ export const IntegrationsListPage = () => { selectedTab: tab as IntegrationsListPageTabs }, params: { - projectId: currentProject?.id + projectId: currentProject?.id, + orgId: currentOrg.id } }); }; @@ -47,7 +49,7 @@ export const IntegrationsListPage = () => {
diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx index 0b2ce65c1..dfc3ce399 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx @@ -39,7 +39,7 @@ import { } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionSub, useOrganization } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { useToggle } from "@app/hooks"; @@ -82,6 +82,7 @@ export const PkiSyncRow = ({ const { syncOption } = usePkiSyncOption(destination); + const { currentOrg } = useOrganization(); const [isIdCopied, setIsIdCopied] = useToggle(false); const handleCopyId = useCallback(() => { @@ -127,7 +128,8 @@ export const PkiSyncRow = ({ to: ROUTE_PATHS.CertManager.PkiSyncDetailsByIDPage.path, params: { syncId: id, - projectId + projectId, + orgId: currentOrg.id } }); }} diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncsTab.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncsTab.tsx index da0bc180f..677a2b2a5 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncsTab.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncsTab.tsx @@ -8,7 +8,7 @@ import { CreatePkiSyncModal } from "@app/components/pki-syncs"; import { Button, Spinner } from "@app/components/v2"; import { DocumentationLinkBadge } from "@app/components/v3"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionSub, useProject } from "@app/context"; +import { ProjectPermissionSub, useOrganization, useProject } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; import { useListPkiSyncs } from "@app/hooks/api/pkiSyncs"; @@ -26,18 +26,19 @@ export const PkiSyncsTab = () => { const navigate = useNavigate(); const { currentProject } = useProject(); - + const { currentOrg } = useOrganization(); const memoizedSearch = useMemo(() => search, [search]); const navigateToBase = useCallback(() => { navigate({ to: ROUTE_PATHS.CertManager.IntegrationsListPage.path, params: { - projectId: currentProject?.id + projectId: currentProject?.id, + orgId: currentOrg.id }, search: memoizedSearch }); - }, [navigate, currentProject?.id, memoizedSearch]); + }, [navigate, currentProject?.id, currentOrg.id, memoizedSearch]); useEffect(() => { if (!addSync) return; @@ -59,7 +60,7 @@ export const PkiSyncsTab = () => { handlePopUpOpen("addSync", { destination: parsedData.destination, initialData }); navigate({ to: ROUTE_PATHS.CertManager.IntegrationsListPage.path, - params: { projectId: currentProject?.id }, + params: { projectId: currentProject?.id, orgId: currentOrg.id }, search: { selectedTab: IntegrationsListPageTabs.PkiSyncs }, replace: true }); @@ -79,7 +80,8 @@ export const PkiSyncsTab = () => { connectionId, connectionName, navigate, - currentProject?.id + currentProject?.id, + currentOrg.id ]); const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs( diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/route.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/route.tsx index a996a23b5..faec782c7 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/route.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/route.tsx @@ -15,7 +15,7 @@ const IntegrationsListPageQuerySchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/" )({ component: IntegrationsListPage, validateSearch: zodValidator(IntegrationsListPageQuerySchema), diff --git a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx index 319323545..c8243b914 100644 --- a/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiCollectionDetailsByIDPage/PkiCollectionDetailsByIDPage.tsx @@ -18,7 +18,12 @@ import { Tooltip } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useOrganization, + useProject +} from "@app/context"; import { useDeletePkiCollection, useGetPkiCollectionById } from "@app/hooks/api"; import { PkiItemType } from "@app/hooks/api/pkiCollections/constants"; import { ProjectType } from "@app/hooks/api/projects/types"; @@ -34,6 +39,7 @@ export const PkiCollectionPage = () => { }); const collectionId = params.collectionId as string; const { currentProject } = useProject(); + const { currentOrg } = useOrganization(); const projectId = currentProject?.id || ""; const { data } = useGetPkiCollectionById(collectionId); @@ -58,8 +64,9 @@ export const PkiCollectionPage = () => { }); handlePopUpClose("deletePkiCollection"); navigate({ - to: "/projects/cert-management/$projectId/policies", + to: "/organizations/$orgId/projects/cert-management/$projectId/policies", params: { + orgId: currentOrg.id, projectId: params.projectId } }); @@ -70,8 +77,9 @@ export const PkiCollectionPage = () => { {data && (
{ @@ -13,8 +13,9 @@ export const Route = createFileRoute( { label: "Certificate Collections", link: linkOptions({ - to: "/projects/cert-management/$projectId/policies", + to: "/organizations/$orgId/projects/cert-management/$projectId/policies", params: { + orgId: params.orgId, projectId: params.projectId } }) diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx index 572ee998e..acea4d4d0 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx @@ -21,6 +21,7 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionPkiSubscriberActions, ProjectPermissionSub, + useOrganization, useProject } from "@app/context"; import { useDeletePkiSubscriber, useGetPkiSubscriber } from "@app/hooks/api"; @@ -32,6 +33,7 @@ import { PkiSubscriberCertificatesSection, PkiSubscriberDetailsSection } from ". const Page = () => { const navigate = useNavigate(); + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const projectId = currentProject.id; const subscriberName = useParams({ @@ -62,8 +64,9 @@ const Page = () => { handlePopUpClose("deletePkiSubscriber"); navigate({ - to: "/projects/cert-management/$projectId/subscribers", + to: "/organizations/$orgId/projects/cert-management/$projectId/subscribers", params: { + orgId: currentOrg.id, projectId } }); @@ -74,8 +77,9 @@ const Page = () => { {data && (
handlePopUpOpen && handlePopUpOpen("revokeCertificate", { - serialNumber: certificate.serialNumber + certificateId: certificate.id }) } disabled={!isAllowed} diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx index 429a10574..75555cbc0 100644 --- a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { PkiSubscriberDetailsByIDPage } from "./PkiSubscriberDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName" )({ component: PkiSubscriberDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -13,8 +13,9 @@ export const Route = createFileRoute( { label: "Subscribers", link: linkOptions({ - to: "/projects/cert-management/$projectId/subscribers", + to: "/organizations/$orgId/projects/cert-management/$projectId/subscribers", params: { + orgId: params.orgId, projectId: params.projectId } }) diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx index 27c91851c..054cbcc45 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx @@ -213,7 +213,8 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => { // Fetch Azure ADCS templates when Azure CA is selected const { data: azureTemplates } = useGetAzureAdcsTemplates({ caId: selectedCa?.type === CaType.AZURE_AD_CS ? selectedCaId : "", - projectId + projectId, + isAzureAdcsCa: true }); // Initialize form with ALL subscriber data including template diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx index 89dfd5f02..880f4af8f 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx @@ -30,6 +30,7 @@ import { Badge } from "@app/components/v3"; import { ProjectPermissionPkiSubscriberActions, ProjectPermissionSub, + useOrganization, useProject } from "@app/context"; import { useListWorkspacePkiSubscribers } from "@app/hooks/api"; @@ -49,6 +50,7 @@ type Props = { export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => { const navigate = useNavigate(); + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const { data, isPending } = useListWorkspacePkiSubscribers(currentProject?.id || ""); return ( @@ -75,8 +77,9 @@ export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => { key={`pki-subscriber-${subscriber.id}`} onClick={() => navigate({ - to: "/projects/cert-management/$projectId/subscribers/$subscriberName", + to: "/organizations/$orgId/projects/cert-management/$projectId/subscribers/$subscriberName", params: { + orgId: currentOrg.id, projectId: currentProject.id, subscriberName: subscriber.name } diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx index c8a2541d8..df39a68a2 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { PkiSubscribersPage } from "./PkiSubscribersPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers/" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers/" )({ component: PkiSubscribersPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/PkiSyncDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/PkiSyncDetailsByIDPage.tsx index 16b105bc6..ab11e2ace 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/PkiSyncDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/PkiSyncDetailsByIDPage.tsx @@ -27,7 +27,7 @@ import { const PageContent = () => { const navigate = useNavigate(); - const { syncId, projectId } = useParams({ + const { syncId, projectId, orgId } = useParams({ from: ROUTE_PATHS.CertManager.PkiSyncDetailsByIDPage.id }); @@ -79,7 +79,8 @@ const PageContent = () => { navigate({ to: ROUTE_PATHS.CertManager.IntegrationsListPage.path, params: { - projectId + projectId, + orgId }, search: { selectedTab: IntegrationsListPageTabs.PkiSyncs diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx index 5171b4d95..8277d3345 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx @@ -36,7 +36,7 @@ import { } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionSub, useOrganization } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { usePopUp, useToggle } from "@app/hooks"; @@ -69,6 +69,7 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { const updatePkiSyncMutation = useUpdatePkiSync(); const { syncOption } = usePkiSyncOption(destination); + const { currentOrg } = useOrganization(); const destinationName = PKI_SYNC_MAP[destination].name; @@ -287,7 +288,8 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { navigate({ to: ROUTE_PATHS.CertManager.IntegrationsListPage.path, params: { - projectId + projectId, + orgId: currentOrg.id }, search: { selectedTab: IntegrationsListPageTabs.PkiSyncs diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncAuditLogsSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncAuditLogsSection.tsx index 6ef633091..4be7e0b3e 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncAuditLogsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncAuditLogsSection.tsx @@ -2,7 +2,7 @@ import { faFingerprint } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link } from "@tanstack/react-router"; -import { useProject, useSubscription } from "@app/context"; +import { useOrganization, useProject, useSubscription } from "@app/context"; import { EventType } from "@app/hooks/api/auditLogs/enums"; import { TPkiSync } from "@app/hooks/api/pkiSyncs"; import { LogsSection } from "@app/pages/organization/AuditLogsPage/components/LogsSection"; @@ -20,7 +20,7 @@ type Props = { export const PkiSyncAuditLogsSection = ({ pkiSync }: Props) => { const { subscription } = useSubscription(); const { currentProject } = useProject(); - + const { currentOrg } = useOrganization(); const auditLogsRetentionDays = subscription?.auditLogsRetentionDays ?? 30; return ( @@ -53,7 +53,12 @@ export const PkiSyncAuditLogsSection = ({ pkiSync }: Props) => {

Please{" "} {subscription && subscription.slug !== null ? ( - + upgrade your subscription diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/route.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/route.tsx index f10ebe94a..49bed6c75 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/route.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/route.tsx @@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations"; import { PkiSyncDetailsByIDPage } from "./index"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId" )({ component: PkiSyncDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,7 +15,7 @@ export const Route = createFileRoute( { label: "Integrations", link: linkOptions({ - to: "/projects/cert-management/$projectId/integrations", + to: "/organizations/$orgId/projects/cert-management/$projectId/integrations", params, search: { selectedTab: IntegrationsListPageTabs.PkiSyncs diff --git a/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx b/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx index cf568bf2c..c943ede97 100644 --- a/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx +++ b/frontend/src/pages/cert-manager/PkiTemplateListPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { PkiTemplateListPage } from "./PkiTemplateListPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/" )({ component: PkiTemplateListPage, beforeLoad: ({ context }) => { 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 516b64288..138e60de3 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -9,6 +9,7 @@ import { createNotification } from "@app/components/notifications"; import { Button, Checkbox, + FilterableSelect, FormControl, Input, Modal, @@ -19,8 +20,11 @@ import { Tooltip } from "@app/components/v2"; import { useProject, useSubscription } from "@app/context"; -import { useListCasByProjectId } from "@app/hooks/api/ca/queries"; +import { CaType } from "@app/hooks/api/ca/enums"; +import { useGetAzureAdcsTemplates, useListCasByProjectId } from "@app/hooks/api/ca/queries"; import { + EnrollmentType, + IssuerType, TCertificateProfileWithDetails, TCreateCertificateProfileDTO, TUpdateCertificateProfileDTO, @@ -46,8 +50,9 @@ const createSchema = z .trim() .max(1000, "Description must be less than 1000 characters") .optional(), - enrollmentType: z.enum(["api", "est", "acme"]), - certificateAuthorityId: z.string().min(1, "Certificate Authority is required"), + enrollmentType: z.nativeEnum(EnrollmentType), + issuerType: z.nativeEnum(IssuerType), + certificateAuthorityId: z.string().nullable().optional(), certificateTemplateId: z.string().min(1, "Certificate Template is required"), estConfig: z .object({ @@ -74,23 +79,110 @@ const createSchema = z renewBeforeDays: z.number().min(1).max(365).optional() }) .optional(), - acmeConfig: z.object({}).optional() + acmeConfig: z.object({}).optional(), + externalConfigs: z + .object({ + template: z.string().min(1, "Azure ADCS template is required") + }) + .optional() }) .refine( (data) => { - if (data.enrollmentType === "est" && !data.estConfig) { - return false; - } - if (data.enrollmentType === "api" && !data.apiConfig) { - return false; - } - if (data.enrollmentType === "acme" && !data.acmeConfig) { - return false; + if (data.enrollmentType === EnrollmentType.EST) { + return !!data.estConfig; } return true; }, { - message: "Configuration is required for selected enrollment type" + message: "EST enrollment type requires EST configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.API) { + return !!data.apiConfig; + } + return true; + }, + { + message: "API enrollment type requires API configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.ACME) { + return !!data.acmeConfig; + } + return true; + }, + { + message: "ACME enrollment type requires ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.EST) { + return !data.apiConfig && !data.acmeConfig; + } + return true; + }, + { + message: "EST enrollment type cannot have API or ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.API) { + return !data.estConfig && !data.acmeConfig; + } + return true; + }, + { + message: "API enrollment type cannot have EST or ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.ACME) { + return !data.estConfig && !data.apiConfig; + } + return true; + }, + { + message: "ACME enrollment type cannot have EST or API configuration" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.CA) { + return !!data.certificateAuthorityId; + } + return true; + }, + { + message: "CA issuer type requires a certificate authority" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.SELF_SIGNED) { + return !data.certificateAuthorityId; + } + return true; + }, + { + message: "Self-signed issuer type cannot have a certificate authority" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.SELF_SIGNED) { + return data.enrollmentType === EnrollmentType.API; + } + return true; + }, + { + message: "Self-signed issuer type only supports API enrollment" } ); @@ -110,8 +202,9 @@ const editSchema = z .trim() .max(1000, "Description must be less than 1000 characters") .optional(), - enrollmentType: z.enum(["api", "est", "acme"]), - certificateAuthorityId: z.string().optional(), + enrollmentType: z.nativeEnum(EnrollmentType), + issuerType: z.nativeEnum(IssuerType), + certificateAuthorityId: z.string().nullable().optional(), certificateTemplateId: z.string().optional(), estConfig: z .object({ @@ -126,23 +219,110 @@ const editSchema = z renewBeforeDays: z.number().min(1).max(365).optional() }) .optional(), - acmeConfig: z.object({}).optional() + acmeConfig: z.object({}).optional(), + externalConfigs: z + .object({ + template: z.string().optional() + }) + .optional() }) .refine( (data) => { - if (data.enrollmentType === "est" && !data.estConfig) { - return false; - } - if (data.enrollmentType === "api" && !data.apiConfig) { - return false; - } - if (data.enrollmentType === "acme" && !data.acmeConfig) { - return false; + if (data.enrollmentType === EnrollmentType.EST) { + return !!data.estConfig; } return true; }, { - message: "Configuration is required for selected enrollment type" + message: "EST enrollment type requires EST configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.API) { + return !!data.apiConfig; + } + return true; + }, + { + message: "API enrollment type requires API configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.ACME) { + return !!data.acmeConfig; + } + return true; + }, + { + message: "ACME enrollment type requires ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.EST) { + return !data.apiConfig && !data.acmeConfig; + } + return true; + }, + { + message: "EST enrollment type cannot have API or ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.API) { + return !data.estConfig && !data.acmeConfig; + } + return true; + }, + { + message: "API enrollment type cannot have EST or ACME configuration" + } + ) + .refine( + (data) => { + if (data.enrollmentType === EnrollmentType.ACME) { + return !data.estConfig && !data.apiConfig; + } + return true; + }, + { + message: "ACME enrollment type cannot have EST or API configuration" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.CA) { + return !!data.certificateAuthorityId; + } + return true; + }, + { + message: "CA issuer type requires a certificate authority" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.SELF_SIGNED) { + return !data.certificateAuthorityId; + } + return true; + }, + { + message: "Self-signed issuer type cannot have a certificate authority" + } + ) + .refine( + (data) => { + if (data.issuerType === IssuerType.SELF_SIGNED) { + return data.enrollmentType === EnrollmentType.API; + } + return true; + }, + { + message: "Self-signed issuer type only supports API enrollment" } ); @@ -171,7 +351,7 @@ export const CreateProfileModal = ({ const { currentProject } = useProject(); const { subscription } = useSubscription(); - const { data: caData } = useListCasByProjectId(currentProject?.id || ""); + const { data: allCaData } = useListCasByProjectId(currentProject?.id || ""); const { data: templateData } = useListCertificateTemplatesV2({ projectId: currentProject?.id || "", limit: 100, @@ -183,9 +363,23 @@ export const CreateProfileModal = ({ const isEdit = mode === "edit" && profile; - const certificateAuthorities = caData || []; + const certificateAuthorities = (allCaData || []).map((ca) => ({ + ...ca, + groupType: ca.type === "internal" ? "internal" : "external" + })); const certificateTemplates = templateData?.certificateTemplates || []; + const getGroupHeaderLabel = (groupType: "internal" | "external") => { + switch (groupType) { + case "internal": + return "Internal CAs"; + case "external": + return "External CAs"; + default: + return ""; + } + }; + const { control, handleSubmit, reset, watch, setValue, formState } = useForm({ resolver: zodResolver(isEdit ? editSchema : createSchema), defaultValues: isEdit @@ -193,10 +387,11 @@ export const CreateProfileModal = ({ slug: profile.slug, description: profile.description || "", enrollmentType: profile.enrollmentType, - certificateAuthorityId: profile.caId, + issuerType: profile.issuerType, + certificateAuthorityId: profile.caId || undefined, certificateTemplateId: profile.certificateTemplateId, estConfig: - profile.enrollmentType === "est" + profile.enrollmentType === EnrollmentType.EST ? { disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false, @@ -205,39 +400,65 @@ export const CreateProfileModal = ({ } : undefined, apiConfig: - profile.enrollmentType === "api" + profile.enrollmentType === EnrollmentType.API ? { autoRenew: profile.apiConfig?.autoRenew || false, renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30 } : undefined, - acmeConfig: profile.enrollmentType === "acme" ? {} : undefined + acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined, + externalConfigs: profile.externalConfigs + ? { + template: + typeof profile.externalConfigs === "object" && + profile.externalConfigs !== null && + typeof profile.externalConfigs.template === "string" + ? profile.externalConfigs.template + : "" + } + : undefined } : { slug: "", description: "", - enrollmentType: "api", + enrollmentType: EnrollmentType.API, + issuerType: IssuerType.CA, certificateAuthorityId: "", certificateTemplateId: "", apiConfig: { autoRenew: false, renewBeforeDays: 30 }, - acmeConfig: {} + acmeConfig: {}, + externalConfigs: undefined } }); const watchedEnrollmentType = watch("enrollmentType"); + const watchedIssuerType = watch("issuerType"); + const watchedCertificateAuthorityId = watch("certificateAuthorityId"); const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation"); const watchedAutoRenew = watch("apiConfig.autoRenew"); + // Get the selected CA to check if it's Azure ADCS + const selectedCa = certificateAuthorities.find((ca) => ca.id === watchedCertificateAuthorityId); + const isAzureAdcsCa = selectedCa?.type === CaType.AZURE_AD_CS; + + // Fetch Azure ADCS templates if needed + const { data: azureAdcsTemplatesData } = useGetAzureAdcsTemplates({ + caId: watchedCertificateAuthorityId || "", + projectId: currentProject?.id || "", + isAzureAdcsCa + }); + useEffect(() => { if (isEdit && profile) { reset({ slug: profile.slug, description: profile.description || "", enrollmentType: profile.enrollmentType, - certificateAuthorityId: profile.caId, + issuerType: profile.issuerType, + certificateAuthorityId: profile.caId || undefined, certificateTemplateId: profile.certificateTemplateId, estConfig: profile.enrollmentType === "est" @@ -255,13 +476,41 @@ export const CreateProfileModal = ({ renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30 } : undefined, - acmeConfig: profile.enrollmentType === "acme" ? {} : undefined + acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined, + externalConfigs: profile.externalConfigs + ? { + template: + typeof profile.externalConfigs === "object" && + profile.externalConfigs !== null && + typeof profile.externalConfigs.template === "string" + ? profile.externalConfigs.template + : "" + } + : undefined }); } - }, [isEdit, profile, reset]); + }, [isEdit, profile, reset, allCaData]); + + // Additional effect to reset external configs when Azure ADCS templates are loaded + useEffect(() => { + if ( + isEdit && + profile && + isAzureAdcsCa && + azureAdcsTemplatesData?.templates && + profile.externalConfigs && + typeof profile.externalConfigs === "object" && + profile.externalConfigs !== null && + typeof profile.externalConfigs.template === "string" + ) { + // Re-set the external configs to ensure the template value is properly set + // after the Azure ADCS templates have been loaded + setValue("externalConfigs.template", profile.externalConfigs.template); + } + }, [isEdit, profile, isAzureAdcsCa, azureAdcsTemplatesData, setValue]); const onFormSubmit = async (data: FormData) => { - if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === "acme") { + if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === EnrollmentType.ACME) { reset(); onClose(); handlePopUpOpen("upgradePlan", { @@ -272,21 +521,39 @@ export const CreateProfileModal = ({ if (!currentProject?.id && !isEdit) return; + // Validate Azure ADCS template requirement + if ( + isAzureAdcsCa && + (!data.externalConfigs?.template || data.externalConfigs.template.trim() === "") + ) { + createNotification({ + text: "Azure ADCS Certificate Authority requires a template to be specified", + type: "error" + }); + return; + } + if (isEdit) { const updateData: TUpdateCertificateProfileDTO = { profileId: profile.id, slug: data.slug, - description: data.description + description: data.description, + issuerType: data.issuerType }; - if (data.enrollmentType === "est" && data.estConfig) { + if (data.enrollmentType === EnrollmentType.EST && data.estConfig) { updateData.estConfig = data.estConfig; - } else if (data.enrollmentType === "api" && data.apiConfig) { + } else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) { updateData.apiConfig = data.apiConfig; - } else if (data.enrollmentType === "acme" && data.acmeConfig) { + } else if (data.enrollmentType === EnrollmentType.ACME && data.acmeConfig) { updateData.acmeConfig = data.acmeConfig; } + // Add external configs if present + if (data.externalConfigs) { + updateData.externalConfigs = data.externalConfigs; + } + await updateProfile.mutateAsync(updateData); } else { if (!currentProject?.id) { @@ -298,22 +565,31 @@ export const CreateProfileModal = ({ slug: data.slug, description: data.description, enrollmentType: data.enrollmentType, - caId: data.certificateAuthorityId, + issuerType: data.issuerType, + caId: + data.issuerType === IssuerType.SELF_SIGNED + ? undefined + : data.certificateAuthorityId || undefined, certificateTemplateId: data.certificateTemplateId }; - if (data.enrollmentType === "est" && data.estConfig) { + if (data.enrollmentType === EnrollmentType.EST && data.estConfig) { createData.estConfig = { passphrase: data.estConfig.passphrase, caChain: data.estConfig.caChain || undefined, disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation }; - } else if (data.enrollmentType === "api" && data.apiConfig) { + } else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) { createData.apiConfig = data.apiConfig; - } else if (data.enrollmentType === "acme" && data.acmeConfig) { + } else if (data.enrollmentType === EnrollmentType.ACME && data.acmeConfig) { createData.acmeConfig = data.acmeConfig; } + // Add external configs if present + if (data.externalConfigs) { + createData.externalConfigs = data.externalConfigs; + } + await createProfile.mutateAsync(createData); } @@ -372,34 +648,125 @@ export const CreateProfileModal = ({ ( )} /> + {watchedIssuerType === "ca" && ( + ( + + ca.id === value) || null} + onChange={(selectedCaValue) => { + if (Array.isArray(selectedCaValue)) { + onChange(selectedCaValue[0]?.id || ""); + } else if ( + selectedCaValue && + typeof selectedCaValue === "object" && + "id" in selectedCaValue + ) { + onChange(selectedCaValue.id || ""); + } else { + onChange(""); + } + }} + getOptionLabel={(ca) => + ca.type === "internal" && ca.configuration.friendlyName + ? ca.configuration.friendlyName + : ca.name + } + getOptionValue={(ca) => ca.id} + options={certificateAuthorities} + groupBy="groupType" + getGroupHeaderLabel={getGroupHeaderLabel} + placeholder="Select a certificate authority" + isDisabled={Boolean(isEdit)} + className="w-full" + /> + + )} + /> + )} + + {/* Azure ADCS Template Selection */} + {isAzureAdcsCa && ( + ( + + template.id === value) || + null + } + onChange={(selectedTemplate) => { + if (Array.isArray(selectedTemplate)) { + onChange(selectedTemplate[0]?.id || ""); + } else if ( + selectedTemplate && + typeof selectedTemplate === "object" && + "id" in selectedTemplate + ) { + onChange(selectedTemplate.id || ""); + } else { + onChange(""); + } + }} + getOptionLabel={(template) => template.name} + getOptionValue={(template) => template.id} + options={azureAdcsTemplatesData?.templates || []} + placeholder="Select an Azure ADCS certificate template" + className="w-full" + /> + + )} + /> + )} + API - EST - ACME + {watchedIssuerType !== IssuerType.SELF_SIGNED && ( + EST + )} + {watchedIssuerType !== IssuerType.SELF_SIGNED && ( + ACME + )} )} diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileList.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileList.tsx index 7b1ac187a..0ba217ce9 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileList.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileList.tsx @@ -54,7 +54,7 @@ export const ProfileList = ({ - + @@ -77,10 +77,10 @@ export const ProfileList = ({ - {isLoading && } + {isLoading && } {!isLoading && (!profiles || profiles.length === 0) && ( - + 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 009ebe1a4..b719c58aa 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/ProfileRow.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ import { useCallback } from "react"; import { faCheck, @@ -29,8 +30,8 @@ import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; import { usePopUp, useToggle } from "@app/hooks"; -import { useGetCaById } from "@app/hooks/api/ca/queries"; -import { TCertificateProfile } from "@app/hooks/api/certificateProfiles"; +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"; @@ -49,7 +50,7 @@ export const ProfileRow = ({ }: Props) => { const { permission } = useProjectPermission(); - const { data: caData } = useGetCaById(profile.caId); + const { data: caData } = useGetInternalCaById(profile.caId ?? ""); const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const); @@ -121,7 +122,13 @@ export const ProfileRow = ({ {getEnrollmentTypeBadge(profile.enrollmentType)} - {caData?.friendlyName || caData?.commonName || profile.caId} + {profile.issuerType === IssuerType.SELF_SIGNED + ? "Self-signed" + : profile.certificateAuthority?.isExternal + ? profile.certificateAuthority.name + : caData?.configuration.friendlyName || + caData?.configuration.commonName || + profile.caId} @@ -175,7 +182,7 @@ export const ProfileRow = ({ }} icon={} > - Issue Certificate + Request Certificate )} {canDeleteProfile && ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/route.tsx b/frontend/src/pages/cert-manager/PoliciesPage/route.tsx index 1db3b90d9..807d69bb9 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/route.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { PoliciesPage } from "./PoliciesPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/policies" )({ component: PoliciesPage, beforeLoad: ({ context }) => { 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/cert-manager/SettingsPage/route.tsx b/frontend/src/pages/cert-manager/SettingsPage/route.tsx index f1400f30e..59eccb028 100644 --- a/frontend/src/pages/cert-manager/SettingsPage/route.tsx +++ b/frontend/src/pages/cert-manager/SettingsPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { SettingsPage } from "./SettingsPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/settings" )({ component: SettingsPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/cert-manager/layout.tsx b/frontend/src/pages/cert-manager/layout.tsx index c8ec6a23b..0462c230d 100644 --- a/frontend/src/pages/cert-manager/layout.tsx +++ b/frontend/src/pages/cert-manager/layout.tsx @@ -8,7 +8,7 @@ import { PkiManagerLayout } from "@app/layouts/PkiManagerLayout"; import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout" )({ component: PkiManagerLayout, beforeLoad: async ({ params, context }) => { diff --git a/frontend/src/pages/kms/KmipPage/route.tsx b/frontend/src/pages/kms/KmipPage/route.tsx index 662bfc32c..cd67ae9ac 100644 --- a/frontend/src/pages/kms/KmipPage/route.tsx +++ b/frontend/src/pages/kms/KmipPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { KmipPage } from "./KmipPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/kms/$projectId/_kms-layout/kmip" )({ component: KmipPage, beforeLoad: ({ context }) => { 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 = () => {
{ diff --git a/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx b/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx index 4c3c12efa..3a0771302 100644 --- a/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/kms/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"; @@ -16,6 +19,8 @@ const tabs = [ export const SettingsPage = () => { 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/kms/SettingsPage/route.tsx b/frontend/src/pages/kms/SettingsPage/route.tsx index b47df3f86..636a3cd79 100644 --- a/frontend/src/pages/kms/SettingsPage/route.tsx +++ b/frontend/src/pages/kms/SettingsPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { SettingsPage } from "./SettingsPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/settings" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/kms/$projectId/_kms-layout/settings" )({ component: SettingsPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/kms/layout.tsx b/frontend/src/pages/kms/layout.tsx index f29a7627a..9e835d479 100644 --- a/frontend/src/pages/kms/layout.tsx +++ b/frontend/src/pages/kms/layout.tsx @@ -8,7 +8,7 @@ import { KmsLayout } from "@app/layouts/KmsLayout"; import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/kms/$projectId/_kms-layout" )({ component: KmsLayout, beforeLoad: async ({ params, context }) => { diff --git a/frontend/src/pages/middlewares/authenticate.tsx b/frontend/src/pages/middlewares/authenticate.tsx index 03005ce05..3998dde63 100644 --- a/frontend/src/pages/middlewares/authenticate.tsx +++ b/frontend/src/pages/middlewares/authenticate.tsx @@ -44,7 +44,7 @@ export const Route = createFileRoute("/_authenticate")({ if ( !data.organizationId && location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path && - location.pathname !== "/organization/none" + location.pathname !== "/organizations/none" ) { throw redirect({ to: "/login/select-organization" }); } diff --git a/frontend/src/pages/middlewares/inject-org-details.tsx b/frontend/src/pages/middlewares/inject-org-details.tsx index d2f3e97ab..35d60a537 100644 --- a/frontend/src/pages/middlewares/inject-org-details.tsx +++ b/frontend/src/pages/middlewares/inject-org-details.tsx @@ -6,8 +6,15 @@ import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subs // Route context to fill in organization's data like details, subscription etc export const Route = createFileRoute("/_authenticate/_inject-org-details")({ - beforeLoad: async ({ context }) => { - const organizationId = context.organizationId!; + beforeLoad: async ({ context, params }) => { + let organizationId: string; + + if ((params as { orgId?: string })?.orgId) { + organizationId = (params as { orgId: string }).orgId; + } else { + organizationId = context.organizationId!; + } + await context.queryClient.ensureQueryData({ queryKey: organizationKeys.getOrgById(organizationId), queryFn: () => fetchOrganizationById(organizationId) diff --git a/frontend/src/pages/middlewares/restrict-login-signup.tsx b/frontend/src/pages/middlewares/restrict-login-signup.tsx index 7d60d9d95..ec1958d14 100644 --- a/frontend/src/pages/middlewares/restrict-login-signup.tsx +++ b/frontend/src/pages/middlewares/restrict-login-signup.tsx @@ -118,7 +118,8 @@ export const Route = createFileRoute("/_restrict-login-signup")({ throw redirect({ to: "/login/select-organization" }); } throw redirect({ - to: "/organization/projects" + to: "/organizations/$orgId/projects", + params: { orgId: data.organizationId } }); }, component: AuthConsentWrapper 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 cc6d7c7aa..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) => { navigate({ - to: "/organization/groups/$groupId", + to: "/organizations/$orgId/groups/$groupId", params: { + orgId, groupId: id } }) @@ -334,8 +335,9 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { icon={} onClick={() => navigate({ - to: "/organization/groups/$groupId", + to: "/organizations/$orgId/groups/$groupId", params: { + orgId, groupId: id } }) diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx index 6db81f19e..6871dd3f3 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx @@ -154,7 +154,9 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) = onOpenChange={handleClose} >
-

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 2f8ce56c5..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) => {
navigate({ - to: "/projects/secret-management/$projectId/integrations/$integrationId", + to: "/organizations/$orgId/projects/secret-management/$projectId/integrations/$integrationId", params: { + orgId: currentOrg.id, integrationId: integration.id, projectId: currentProject.id } 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 db4c6356e..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,10 +1,7 @@ -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 { Checkbox, DeleteActionModal, Spinner } from "@app/components/v2"; import { useProject } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; import { @@ -17,37 +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 { 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 { @@ -57,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; @@ -70,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 && @@ -80,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(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, @@ -116,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) @@ -149,18 +96,10 @@ export const NativeIntegrationsTab = () => { return ( <> - {view === IntegrationView.List ? ( + {integrations?.length && (

Native Integrations

-
{ }} />
- ) : ( - setView(IntegrationView.List)} - /> )} { const navigate = useNavigate(); + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); useEffect(() => { @@ -35,7 +36,8 @@ export const SecretSyncsTab = () => { navigate({ to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path, params: { - projectId: currentProject.id + projectId: currentProject.id, + orgId: currentOrg.id }, search }); @@ -66,6 +68,7 @@ export const SecretSyncsTab = () => { navigate({ to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path, params: { + orgId: currentOrg.id, projectId: currentProject.id }, search diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx index 405aec3e9..0d5f66c26 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/route.tsx @@ -23,11 +23,11 @@ const IntegrationsListPageQuerySchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/integrations/" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/" )({ component: IntegrationsListPage, validateSearch: zodValidator(IntegrationsListPageQuerySchema), - beforeLoad: async ({ context, search, params: { projectId } }) => { + beforeLoad: async ({ context, search, params: { projectId, orgId } }) => { if (!search.selectedTab) { let secretSyncs: TSecretSync[]; @@ -38,20 +38,16 @@ export const Route = createFileRoute( }); } catch { throw redirect({ - to: "/projects/secret-management/$projectId/integrations", - params: { - projectId - }, + to: "/organizations/$orgId/projects/secret-management/$projectId/integrations", + params: { orgId, projectId }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations } }); } if (secretSyncs.length) { throw redirect({ - to: "/projects/secret-management/$projectId/integrations", - params: { - projectId - }, + to: "/organizations/$orgId/projects/secret-management/$projectId/integrations", + params: { orgId, projectId }, search: { selectedTab: IntegrationsListPageTabs.SecretSyncs } }); } @@ -64,8 +60,9 @@ export const Route = createFileRoute( }); } catch { throw redirect({ - to: "/projects/secret-management/$projectId/integrations", + to: "/organizations/$orgId/projects/secret-management/$projectId/integrations", params: { + orgId, projectId }, search: { selectedTab: IntegrationsListPageTabs.SecretSyncs } @@ -74,8 +71,9 @@ export const Route = createFileRoute( if (integrations.length) { throw redirect({ - to: "/projects/secret-management/$projectId/integrations", + to: "/organizations/$orgId/projects/secret-management/$projectId/integrations", params: { + orgId, projectId }, search: { selectedTab: IntegrationsListPageTabs.NativeIntegrations } @@ -83,8 +81,9 @@ export const Route = createFileRoute( } throw redirect({ - to: "/projects/secret-management/$projectId/integrations", + to: "/organizations/$orgId/projects/secret-management/$projectId/integrations", params: { + orgId, projectId }, search: { selectedTab: IntegrationsListPageTabs.SecretSyncs } diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 0e02ae8b1..a7c9bf223 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -10,6 +10,8 @@ import { faArrowRight, faArrowRightToBracket, faArrowUp, + faCheck, + faCopy, faFilter, faFingerprint, faFolder, @@ -20,7 +22,7 @@ import { faRotate } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router"; +import { Link, useNavigate, useParams, useRouter, useSearch } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; @@ -79,6 +81,7 @@ import { usePopUp, useResetPageHelper, useResizableHeaderHeight, + useTimedReset, useToggle } from "@app/hooks"; import { @@ -165,6 +168,11 @@ export const OverviewPage = () => { search: el.search }) }); + + const orgId = useParams({ + from: ROUTE_PATHS.SecretManager.OverviewPage.id, + select: (el) => el.orgId + }); const [scrollOffset, setScrollOffset] = useState(0); const [debouncedScrollOffset] = useDebounce(scrollOffset); const { permission } = useProjectPermission(); @@ -189,6 +197,15 @@ export const OverviewPage = () => { } }; + 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 @@ -658,8 +675,9 @@ export const OverviewPage = () => { const envIndex = visibleEnvs.findIndex((el) => slug === el.slug); if (envIndex !== -1) { navigate({ - to: "/projects/secret-management/$projectId/secrets/$envSlug", + to: "/organizations/$orgId/projects/secret-management/$projectId/secrets/$envSlug", params: { + orgId, projectId, envSlug: slug }, @@ -915,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}`} >
{ iconSize="3x" > { const navigate = useNavigate({ - from: "/projects/secret-management/$projectId/overview" + from: "/organizations/$orgId/projects/secret-management/$projectId/overview" }); const onFolderCrumbClick = (index: number) => { diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index 1a33bbca1..48d2eacd2 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -10,7 +10,6 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useQueryClient } from "@tanstack/react-query"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; @@ -33,11 +32,7 @@ import { } from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp, useToggle } from "@app/hooks"; -import { - dashboardKeys, - fetchSecretValue, - useGetSecretValue -} from "@app/hooks/api/dashboard/queries"; +import { useGetSecretValue } from "@app/hooks/api/dashboard/queries"; import { ProjectEnv, SecretType, SecretV3RawSanitized } from "@app/hooks/api/types"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { CollapsibleSecretImports } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports"; @@ -109,8 +104,6 @@ export const SecretEditRow = ({ "editSecret" ] as const); - const queryClient = useQueryClient(); - const { currentProject } = useProject(); const [isFieldFocused, setIsFieldFocused] = useToggle(); @@ -137,19 +130,18 @@ export const SecretEditRow = ({ const { data: secretValueData, isPending: isPendingSecretValueData, - isError: isErrorFetchingSecretValue + isError: isErrorFetchingSecretValue, + refetch: refetchSecretValue } = useGetSecretValue(fetchSecretValueParams, { enabled: canFetchValue && (isVisible || isFieldFocused) }); const isFetchingSecretValue = canFetchValue && isPendingSecretValueData; - const isSecretValueFetched = Boolean(secretValueData); const { handleSubmit, control, reset, - getValues, setValue, formState: { isDirty, isSubmitting } } = useForm({ @@ -178,34 +170,17 @@ export const SecretEditRow = ({ }; const handleCopySecretToClipboard = async () => { - 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/OverviewPage/components/SecretSearchInput/components/QuickSearchDynamicSecretItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchDynamicSecretItem.tsx index 06dff9d3f..6084ea261 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchDynamicSecretItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchDynamicSecretItem.tsx @@ -17,7 +17,7 @@ export const QuickSearchDynamicSecretItem = ({ onClose }: Props) => { const navigate = useNavigate({ - from: "/projects/secret-management/$projectId/overview" + from: "/organizations/$orgId/projects/secret-management/$projectId/overview" }); const [groupDynamicSecret] = dynamicSecretGroup; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchFolderItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchFolderItem.tsx index 55b21b162..d8b03e280 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchFolderItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchFolderItem.tsx @@ -13,7 +13,7 @@ type Props = { export const QuickSearchFolderItem = ({ folderGroup, onClose }: Props) => { const navigate = useNavigate({ - from: "/projects/secret-management/$projectId/overview" + from: "/organizations/$orgId/projects/secret-management/$projectId/overview" }); const [groupFolder] = folderGroup; diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx index e9b73508b..4f919d2d3 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretItem.tsx @@ -48,7 +48,9 @@ export const QuickSearchSecretItem = ({ isSingleEnv, search }: Props) => { - const navigate = useNavigate({ from: "/projects/secret-management/$projectId/overview" }); + const navigate = useNavigate({ + from: "/organizations/$orgId/projects/secret-management/$projectId/overview" + }); const envSlugMap = new Map(environments.map((env) => [env.slug, env])); const [isUrlCopied, , setIsUrlCopied] = useTimedReset({ initialState: false diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretRotationItem.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretRotationItem.tsx index 8790f1c74..afa66611a 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretRotationItem.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretSearchInput/components/QuickSearchSecretRotationItem.tsx @@ -13,7 +13,7 @@ type Props = { export const QuickSearchSecretRotationItem = ({ secretRotationGroup, onClose }: Props) => { const navigate = useNavigate({ - from: "/projects/secret-management/$projectId/overview" + from: "/organizations/$orgId/projects/secret-management/$projectId/overview" }); const [groupSecretRotation] = secretRotationGroup; diff --git a/frontend/src/pages/secret-manager/OverviewPage/route.tsx b/frontend/src/pages/secret-manager/OverviewPage/route.tsx index 2be6c6f25..ed08cfb8a 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/route.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/route.tsx @@ -12,7 +12,7 @@ const SecretOverviewPageQuerySchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/overview" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/overview" )({ component: OverviewPage, validateSearch: zodValidator(SecretOverviewPageQuerySchema), @@ -26,7 +26,7 @@ export const Route = createFileRoute( { label: "Secrets", link: linkOptions({ - to: "/projects/secret-management/$projectId/overview", + to: "/organizations/$orgId/projects/secret-management/$projectId/overview", params }) } diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx index dfc6d4e4d..60f183e2e 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/route.tsx @@ -9,7 +9,7 @@ const SecretApprovalPageQueryParams = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/approval" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/approval" )({ component: SecretApprovalsPage, validateSearch: zodValidator(SecretApprovalPageQueryParams), diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 203a3f6eb..77ea94f99 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -30,6 +30,7 @@ import { ProjectPermissionActions, ProjectPermissionDynamicSecretActions, ProjectPermissionSub, + useOrganization, useProject, useProjectPermission } from "@app/context"; @@ -101,6 +102,7 @@ const LOADER_TEXT = [ ]; const Page = () => { + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const navigate = useNavigate({ from: ROUTE_PATHS.SecretManager.SecretDashboardPage.path @@ -119,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(); @@ -163,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, @@ -267,8 +294,9 @@ const Page = () => { type: "error" }); navigate({ - to: "/projects/secret-management/$projectId/overview", + to: "/organizations/$orgId/projects/secret-management/$projectId/overview", params: { + orgId: currentOrg.id, projectId } }); @@ -444,8 +472,9 @@ const Page = () => { const handleOnClickRollbackMode = () => { if (isPITEnabled) { navigate({ - to: "/projects/secret-management/$projectId/commits/$environment/$folderId", + to: "/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId", params: { + orgId: currentOrg.id, projectId, folderId, environment @@ -807,8 +836,9 @@ const Page = () => { return (
{ )} {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/EnvironmentTabs/EnvironmentTabs.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx index f8a201717..ed8a5ae88 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/EnvironmentTabs/EnvironmentTabs.tsx @@ -24,6 +24,7 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionSub, + useOrganization, useProject, useSubscription } from "@app/context"; @@ -45,6 +46,7 @@ type Props = { const TABS_TO_SHOW = 5; export const EnvironmentTabs = ({ secretPath }: Props) => { + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const currentEnv = useParams({ from: ROUTE_PATHS.SecretManager.SecretDashboardPage.id, @@ -96,7 +98,8 @@ export const EnvironmentTabs = ({ secretPath }: Props) => { to: ROUTE_PATHS.SecretManager.SecretDashboardPage.path, params: { envSlug, - projectId: currentProject.id + projectId: currentProject.id, + orgId: currentOrg.id }, search: (prev) => prev }); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx index 7dc189c42..bb4488a67 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/FolderBreadCrumbs/FolderBreadCrumbs.tsx @@ -8,7 +8,7 @@ type Props = { export const FolderBreadCrumbs = ({ secretPath = "/" }: Props) => { const navigate = useNavigate({ - from: "/projects/secret-management/$projectId/secrets/$envSlug" + from: "/organizations/$orgId/projects/secret-management/$projectId/secrets/$envSlug" }); const onFolderCrumbClick = (index: number) => { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 3cb2a59ca..5a9b522a3 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -46,6 +46,7 @@ import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { ProjectPermissionActions, ProjectPermissionSub, + useOrganization, useProject, useProjectPermission, useSubscription @@ -98,6 +99,7 @@ export const SecretDetailSidebar = ({ secretPath, handleSecretShare }: Props) => { + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const [isFieldFocused, setIsFieldFocused] = useToggle(); const queryClient = useQueryClient(); @@ -776,6 +778,7 @@ export const SecretDetailSidebar = ({ `${getProjectBaseURL(currentProject.type)}/identities/$identityId` as const } params={{ + orgId: currentOrg.id, projectId: currentProject.id, identityId: identity.id }} @@ -806,8 +809,9 @@ export const SecretDetailSidebar = ({ className="z-100" > { - 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 da48a04f0..df054b6e2 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/route.tsx @@ -12,16 +12,25 @@ 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() }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/secrets/$envSlug" )({ component: SecretDashboardPage, validateSearch: zodValidator(SecretDashboardPageQueryParamsSchema), search: { - middlewares: [stripSearchParams({ secretPath: "/", search: "", tags: "", filterBy: "" })] + middlewares: [ + stripSearchParams({ + secretPath: "/", + search: "", + tags: "", + filterBy: "", + dynamicSecretId: "" + }) + ] }, beforeLoad: ({ context, params, search }) => { const secretPathSegments = search.secretPath.split("/").filter(Boolean); @@ -31,7 +40,7 @@ export const Route = createFileRoute( { label: "Secrets", link: linkOptions({ - to: "/projects/secret-management/$projectId/overview", + to: "/organizations/$orgId/projects/secret-management/$projectId/overview", params }) }, @@ -42,8 +51,9 @@ export const Route = createFileRoute( links: context.project.environments.map((el) => ({ label: el.name, link: linkOptions({ - to: "/projects/secret-management/$projectId/secrets/$envSlug", + to: "/organizations/$orgId/projects/secret-management/$projectId/secrets/$envSlug", params: { + orgId: params.orgId, projectId: params.projectId, envSlug: el.slug } diff --git a/frontend/src/pages/secret-manager/SecretRotationPage/SecretRotationPage.tsx b/frontend/src/pages/secret-manager/SecretRotationPage/SecretRotationPage.tsx index 12c264004..c4f8a9cfe 100644 --- a/frontend/src/pages/secret-manager/SecretRotationPage/SecretRotationPage.tsx +++ b/frontend/src/pages/secret-manager/SecretRotationPage/SecretRotationPage.tsx @@ -40,6 +40,7 @@ import { import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; import { ProjectPermissionSub, + useOrganization, useProject, useProjectPermission, useSubscription @@ -57,6 +58,7 @@ import { TSecretRotationProviderTemplate } from "@app/hooks/api/secretRotation/t import { CreateRotationForm } from "@app/pages/secret-manager/SecretRotationPage/components/CreateRotationForm"; const Page = () => { + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const { permission } = useProjectPermission(); @@ -158,8 +160,8 @@ const Page = () => { PostgreSQL and Microsoft SQL Server Rotations can now be created from the{" "} Secret Manager Dashboard {" "} @@ -393,8 +395,8 @@ const Page = () => { Infisical is revamping its Secret Rotation experience. Navigate to the{" "} Secret Manager Dashboard {" "} @@ -410,8 +412,8 @@ const Page = () => {
- Role + Organization Role {
- Role + Organization Role { key={`identity-${id}`} onClick={() => navigate({ - to: "/organization/identities/$identityId", + to: "/organizations/$orgId/identities/$identityId", params: { - identityId: id + identityId: id, + orgId } }) } @@ -397,15 +398,16 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { onClick={(e) => { e.stopPropagation(); navigate({ - to: "/organization/identities/$identityId", + to: "/organizations/$orgId/identities/$identityId", params: { - identityId: id + identityId: id, + orgId } }); }} isDisabled={!isAllowed} > - Edit Identity {isSubOrgIdentity ? "" : "Membership"} + Edit Machine Identity {isSubOrgIdentity ? "" : "Membership"} )} @@ -426,7 +428,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { icon={} > {isSubOrgIdentity - ? "Delete Identity" + ? "Delete Machine Identity" : "Remove From Sub-Organization"} )} @@ -453,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/MachineAuthTemplateUsagesModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/MachineAuthTemplateUsagesModal.tsx index 909391474..bf6eba63e 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/MachineAuthTemplateUsagesModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/MachineAuthTemplateUsagesModal.tsx @@ -61,9 +61,10 @@ export const MachineAuthTemplateUsagesModal = ({ key={`usage-${usage.identityId}`} onClick={() => navigate({ - to: "/organization/identities/$identityId", + to: "/organizations/$orgId/identities/$identityId", params: { - identityId: usage.identityId + identityId: usage.identityId, + orgId: organizationId } }) } 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 24406382e..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 @@ -64,9 +64,10 @@ export const OrgIdentityLinkForm = ({ onClose }: Props) => { type: "success" }); navigate({ - to: "/organization/identities/$identityId", + to: "/organizations/$orgId/identities/$identityId", params: { - identityId: identity.id + identityId: identity.id, + orgId: currentOrg.id } }); }; @@ -77,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 9c54a1e54..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 @@ -155,15 +155,16 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => { handlePopUpToggle("identity", false); navigate({ - to: "/organization/identities/$identityId", + to: "/organizations/$orgId/identities/$identityId", params: { - identityId: createdId + identityId: createdId, + orgId } }); } createNotification({ - text: `Successfully ${popUp?.identity?.data ? "updated" : "created"} identity`, + text: `Successfully ${popUp?.identity?.data ? "updated" : "created"} machine identity`, type: "success" }); @@ -254,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 ea0465bb8..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 navigate({ - to: "/organization/members/$membershipId" as const, + to: "/organizations/$orgId/members/$membershipId" as const, params: { - membershipId: orgMembershipId + membershipId: orgMembershipId, + orgId } }) } @@ -619,9 +620,10 @@ export const OrgMembersTable = ({ onClick={(e) => { e.stopPropagation(); navigate({ - to: "/organization/members/$membershipId" as const, + to: "/organizations/$orgId/members/$membershipId" as const, params: { - membershipId: orgMembershipId + membershipId: orgMembershipId, + orgId } }); }} @@ -725,8 +727,8 @@ export const OrgMembersTable = ({ diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index 9579ce6d6..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" /> @@ -280,9 +280,10 @@ export const OrgRoleTable = () => { className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700" onClick={() => navigate({ - to: "/organization/roles/$roleId", + to: "/organizations/$orgId/roles/$roleId", params: { - roleId: id + roleId: id, + orgId } }) } @@ -339,9 +340,10 @@ export const OrgRoleTable = () => { onClick={(e) => { e.stopPropagation(); navigate({ - to: "/organization/roles/$roleId", + to: "/organizations/$orgId/roles/$roleId", params: { - roleId: id + roleId: id, + orgId } }); }} @@ -439,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 3b1710fdb..6fa3d8854 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"; @@ -149,6 +150,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { return ; case AppConnection.Cloudflare: return ; + case AppConnection.DNSMadeEasy: + return ; case AppConnection.Bitbucket: return ; case AppConnection.Zabbix: @@ -309,6 +312,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/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx index 8a9f1f775..b8caa8678 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionRow.tsx @@ -29,7 +29,7 @@ import { Tr } from "@app/components/v2"; import { Badge } from "@app/components/v3"; -import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context"; +import { OrgPermissionSubjects, ProjectPermissionSub, useOrganization } from "@app/context"; import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types"; import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; @@ -52,6 +52,7 @@ export const AppConnectionRow = ({ onEditDetails, isProjectView }: Props) => { + const { currentOrg } = useOrganization(); const { id, name, method, app, description, isPlatformManagedCredentials, project } = appConnection; @@ -127,6 +128,7 @@ export const AppConnectionRow = ({ // @ts-expect-error app-connections aren't in kms/ssh to={`${getProjectBaseURL(project.type)}/app-connections`} params={{ + orgId: currentOrg?.id || "", projectId: project.id }} className="underline" diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx index 86dfe4530..a036c1e89 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/route.tsx @@ -4,7 +4,7 @@ import { z } from "zod"; import { AppConnectionsPage } from "./AppConnectionsPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/app-connections/" )({ component: AppConnectionsPage, validateSearch: z.object({ diff --git a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/route.tsx b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/route.tsx index 4a4dfa848..558a92091 100644 --- a/frontend/src/pages/organization/AppConnections/OauthCallbackPage/route.tsx +++ b/frontend/src/pages/organization/AppConnections/OauthCallbackPage/route.tsx @@ -11,7 +11,7 @@ const GitHubOAuthCallbackPageQueryParamsSchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/app-connections/$appConnection/oauth/callback" )({ component: OAuthCallbackPage, validateSearch: zodValidator(GitHubOAuthCallbackPageQueryParamsSchema), 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/AuditLogsPage/route.tsx b/frontend/src/pages/organization/AuditLogsPage/route.tsx index 3646b4d16..829ba6066 100644 --- a/frontend/src/pages/organization/AuditLogsPage/route.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { AuditLogsPage } from "./AuditLogsPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/audit-logs" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/audit-logs" )({ component: AuditLogsPage, context: () => ({ diff --git a/frontend/src/pages/organization/BillingPage/route.tsx b/frontend/src/pages/organization/BillingPage/route.tsx index 605a8a949..c271351c1 100644 --- a/frontend/src/pages/organization/BillingPage/route.tsx +++ b/frontend/src/pages/organization/BillingPage/route.tsx @@ -3,13 +3,14 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; import { BillingPage } from "./BillingPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/billing" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/billing" )({ component: BillingPage, - beforeLoad: ({ search }) => { + beforeLoad: ({ search, params }) => { if (search.subOrganization) { throw redirect({ - to: "/organization/projects", + to: "/organizations/$orgId/projects", + params: { orgId: params.orgId }, search }); } diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx index d925f20b0..8c606f07c 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx @@ -45,7 +45,7 @@ const Page = () => { const { data, isPending } = useGetGroupById(groupId); - const { isSubOrganization } = useOrganization(); + const { isSubOrganization, currentOrg } = useOrganization(); const { mutateAsync: deleteMutateAsync } = useDeleteGroup(); @@ -63,7 +63,8 @@ const Page = () => { type: "success" }); navigate({ - to: "/organization/access-management" as const, + to: "/organizations/$orgId/access-management" as const, + params: { orgId: currentOrg.id }, search: { selectedTab: TabSections.Groups } @@ -79,14 +80,15 @@ const Page = () => { {data && (
- Groups + Organization Groups ({ + context: ({ params }) => ({ breadcrumbs: [ { label: "Access Control", - link: linkOptions({ to: "/organization/access-management" }) + link: linkOptions({ + to: "/organizations/$orgId/access-management" as const, + params + }) }, { label: "groups" diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 4fdc55f07..47428f2b8 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -55,13 +55,14 @@ const Page = () => { }); createNotification({ - text: "Successfully deleted identity", + text: "Successfully deleted machine identity", type: "success" }); handlePopUpClose("deleteIdentity"); navigate({ - to: "/organization/access-management", + to: "/organizations/$orgId/access-management" as const, + params: { orgId }, search: { selectedTab: OrgAccessControlTabSections.Identities } @@ -73,18 +74,19 @@ const Page = () => { {data && (
- Identities + Organization Machine Identities
@@ -109,7 +111,7 @@ const Page = () => { }) } > - Unlink Identity + Unlink Machine Identity )} @@ -140,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/IdentityProjectRow.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx index 6bb25e2c1..fe62a7902 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectRow.tsx @@ -6,6 +6,7 @@ import { format } from "date-fns"; import { createNotification } from "@app/components/notifications"; import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; +import { useOrganization } from "@app/context"; import { getProjectBaseURL } from "@app/helpers/project"; import { formatProjectRoleName } from "@app/helpers/roles"; import { useGetUserProjects } from "@app/hooks/api"; @@ -33,6 +34,7 @@ export const IdentityProjectRow = ({ }: Props) => { const { data: workspaces } = useGetUserProjects(); const navigate = useNavigate(); + const { currentOrg } = useOrganization(); const isAccessible = useMemo(() => { const workspaceIds = new Map(); @@ -53,6 +55,7 @@ export const IdentityProjectRow = ({ navigate({ to: `${getProjectBaseURL(project.type)}/access-management` as const, params: { + orgId: currentOrg?.id || "", projectId: project.id }, search: { 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/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx index d0d0b0fcd..5d7bf30e9 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx @@ -22,6 +22,7 @@ import { Tooltip, Tr } from "@app/components/v2"; +import { CopyButton } from "@app/components/v2/CopyButton"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, @@ -153,6 +154,7 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
+ ({ + context: ({ params }) => ({ breadcrumbs: [ { label: "Access Control", - link: linkOptions({ to: "/organization/access-management" }) + link: linkOptions({ to: "/organizations/$orgId/access-management" as const, params }) }, { label: "Identities" 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/NetworkingPage/route.tsx b/frontend/src/pages/organization/NetworkingPage/route.tsx index fb81e3725..09b089ba7 100644 --- a/frontend/src/pages/organization/NetworkingPage/route.tsx +++ b/frontend/src/pages/organization/NetworkingPage/route.tsx @@ -10,7 +10,7 @@ const NetworkingPageQueryParams = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/networking" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/networking" )({ component: NetworkingPage, validateSearch: zodValidator(NetworkingPageQueryParams), diff --git a/frontend/src/pages/organization/NoOrgPage/route.tsx b/frontend/src/pages/organization/NoOrgPage/route.tsx index 5d34fed93..597fcc931 100644 --- a/frontend/src/pages/organization/NoOrgPage/route.tsx +++ b/frontend/src/pages/organization/NoOrgPage/route.tsx @@ -2,6 +2,6 @@ import { createFileRoute } from "@tanstack/react-router"; import { NoOrgPage } from "./NoOrgPage"; -export const Route = createFileRoute("/_authenticate/organization/none")({ +export const Route = createFileRoute("/_authenticate/organizations/none")({ component: NoOrgPage }); diff --git a/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx b/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx index aba2e752d..c0117b758 100644 --- a/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx +++ b/frontend/src/pages/organization/ProjectsPage/ProjectsPage.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; +import { Outlet, useMatches } from "@tanstack/react-router"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { NewProjectModal } from "@app/components/projects"; @@ -27,6 +28,17 @@ import { ProjectListView } from "./components/ProjectListToggle"; export const ProjectsPage = () => { const { t } = useTranslation(); + const matches = useMatches(); + + const hasChildRoute = matches.some( + (match) => + match.pathname.includes("/secret-management/") || + match.pathname.includes("/cert-management/") || + match.pathname.includes("/kms/") || + match.pathname.includes("/pam/") || + match.pathname.includes("/ssh/") || + match.pathname.includes("/secret-scanning/") + ); const [projectListView, setProjectListView] = useState(() => { const storedView = localStorage.getItem("projectListView"); @@ -57,6 +69,10 @@ export const ProjectsPage = () => { ? subscription.workspacesUsed < subscription.workspaceLimit : true; + if (hasChildRoute) { + return ; + } + return (
@@ -65,7 +81,7 @@ export const ProjectsPage = () => { {projectListView === ProjectListView.MyProjects ? ( diff --git a/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx index defc576f4..d775f256a 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/AllProjectView.tsx @@ -30,7 +30,7 @@ import { Tooltip } from "@app/components/v2"; import { Badge } from "@app/components/v3"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionAdminConsoleAction } from "@app/context/OrgPermissionContext/types"; import { getProjectHomePage, getProjectLottieIcon, getProjectTitle } from "@app/helpers/project"; import { @@ -62,6 +62,7 @@ export const AllProjectView = ({ onProjectListViewChange }: Props) => { const navigate = useNavigate(); + const { currentOrg } = useOrganization(); const [searchFilter, setSearchFilter] = useState(""); const [debouncedSearch] = useDebounce(searchFilter); const [projectTypeFilter, setProjectTypeFilter] = useState(); @@ -100,7 +101,8 @@ export const AllProjectView = ({ const handleAccessProject = async ( type: ProjectType, projectId: string, - environments: ProjectEnv[] + environments: ProjectEnv[], + orgId: string ) => { await orgAdminAccessProject.mutateAsync({ projectId @@ -108,6 +110,7 @@ export const AllProjectView = ({ await navigate({ to: getProjectHomePage(type, environments), params: { + orgId, projectId } }); @@ -263,6 +266,7 @@ export const AllProjectView = ({ navigate({ to: getProjectHomePage(workspace.type, workspace.environments), params: { + orgId: currentOrg?.id || "", projectId: workspace.id } }); @@ -273,6 +277,7 @@ export const AllProjectView = ({ navigate({ to: getProjectHomePage(workspace.type, workspace.environments), params: { + orgId: currentOrg?.id || "", projectId: workspace.id } }); @@ -317,7 +322,12 @@ export const AllProjectView = ({ onClick={(e) => { e.stopPropagation(); e.preventDefault(); - handleAccessProject(workspace.type, workspace.id, workspace.environments); + handleAccessProject( + workspace.type, + workspace.id, + workspace.environments, + workspace.orgId + ); }} disabled={ orgAdminAccessProject.variables?.projectId === workspace.id && diff --git a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx index cfbb1b25f..c59ecf57a 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx @@ -180,6 +180,7 @@ export const MyProjectView = ({ navigate({ to: getProjectHomePage(workspace.type, workspace.environments), params: { + orgId: currentOrg?.id || "", projectId: workspace.id } }); @@ -231,6 +232,7 @@ export const MyProjectView = ({ navigate({ to: getProjectHomePage(workspace.type, workspace.environments), params: { + orgId: currentOrg?.id || "", projectId: workspace.id } }); diff --git a/frontend/src/pages/organization/ProjectsPage/route.tsx b/frontend/src/pages/organization/ProjectsPage/route.tsx index b500d4e1d..e1ae5c203 100644 --- a/frontend/src/pages/organization/ProjectsPage/route.tsx +++ b/frontend/src/pages/organization/ProjectsPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { ProjectsPage } from "./ProjectsPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/projects" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects" )({ component: ProjectsPage, context: () => ({ diff --git a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx index 78c5393a7..ff3594ef3 100644 --- a/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/RoleByIDPage.tsx @@ -56,7 +56,8 @@ export const Page = () => { handlePopUpClose("deleteOrgRole"); navigate({ - to: "/organization/access-management" as const, + to: "/organizations/$orgId/access-management" as const, + params: { orgId }, search: { selectedTab: OrgAccessControlTabSections.Roles } @@ -70,7 +71,8 @@ export const Page = () => { {data && (
{ }); navigate({ - to: "/organization/roles/$roleId", + to: "/organizations/$orgId/roles/$roleId" as const, params: { + orgId: role.orgId, roleId: newRole.id } }); diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx index 73da4f161..f47d5ed2c 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RoleModal.tsx @@ -97,8 +97,9 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { handlePopUpToggle("role", false); navigate({ - to: "/organization/roles/$roleId", + to: "/organizations/$orgId/roles/$roleId" as const, params: { + orgId, roleId: newRole.id } }); diff --git a/frontend/src/pages/organization/RoleByIDPage/route.tsx b/frontend/src/pages/organization/RoleByIDPage/route.tsx index c9036c500..94664fb51 100644 --- a/frontend/src/pages/organization/RoleByIDPage/route.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/route.tsx @@ -3,14 +3,17 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { RoleByIDPage } from "./RoleByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/roles/$roleId" )({ component: RoleByIDPage, - context: () => ({ + context: ({ params }) => ({ breadcrumbs: [ { label: "Access Control", - link: linkOptions({ to: "/organization/access-management" }) + link: linkOptions({ + to: "/organizations/$orgId/access-management", + params: { orgId: params.orgId } + }) }, { label: "Roles" diff --git a/frontend/src/pages/organization/SecretSharingPage/ShareSecretSection.tsx b/frontend/src/pages/organization/SecretSharingPage/ShareSecretSection.tsx index 134e341a6..7b8b16860 100644 --- a/frontend/src/pages/organization/SecretSharingPage/ShareSecretSection.tsx +++ b/frontend/src/pages/organization/SecretSharingPage/ShareSecretSection.tsx @@ -17,7 +17,7 @@ enum SecretSharingPageTabs { export const ShareSecretSection = () => { const navigate = useNavigate(); - const { isSubOrganization } = useOrganization(); + const { isSubOrganization, currentOrg } = useOrganization(); const { selectedTab } = useSearch({ from: ROUTE_PATHS.Organization.SecretSharing.id @@ -26,6 +26,7 @@ export const ShareSecretSection = () => { const updateSelectedTab = (tab: string) => { navigate({ to: ROUTE_PATHS.Organization.SecretSharing.path, + params: { orgId: currentOrg.id }, search: (prev) => ({ ...prev, selectedTab: tab as SecretSharingPageTabs }) }); }; diff --git a/frontend/src/pages/organization/SecretSharingPage/route.tsx b/frontend/src/pages/organization/SecretSharingPage/route.tsx index 728fce0b6..05476a117 100644 --- a/frontend/src/pages/organization/SecretSharingPage/route.tsx +++ b/frontend/src/pages/organization/SecretSharingPage/route.tsx @@ -9,7 +9,7 @@ const SecretSharingQueryParams = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/secret-sharing/" )({ component: SecretSharingPage, diff --git a/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx b/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx index 5efec93d9..054455e05 100644 --- a/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx +++ b/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/OauthCallbackPage.tsx @@ -57,7 +57,8 @@ export const OAuthCallbackPage = () => { }); navigate({ - to: ROUTE_PATHS.Organization.SettingsPage.path + to: ROUTE_PATHS.Organization.SettingsPage.path, + params: { orgId: currentOrg.id } }); }, []); diff --git a/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/route.tsx b/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/route.tsx index 8c6af10f3..5e479303e 100644 --- a/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/route.tsx +++ b/frontend/src/pages/organization/SettingsPage/OauthCallbackPage/route.tsx @@ -20,7 +20,7 @@ const SettingsOAuthCallbackPageQueryParamsSchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/settings/oauth/callback" )({ component: OAuthCallbackPage, validateSearch: zodValidator(SettingsOAuthCallbackPageQueryParamsSchema) 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/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx index aa7765db8..070d9929d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx @@ -17,6 +17,7 @@ import { THead, Tr } from "@app/components/v2"; +import { useOrganization } from "@app/context"; import { useListAppConnections } from "@app/hooks/api/appConnections/queries"; import { useDeleteVaultExternalMigrationConfig, @@ -33,6 +34,8 @@ export const VaultConnectionSection = () => { const [configToDelete, setConfigToDelete] = useState(null); const { data: configs = [], isPending: isLoadingConfigs } = useGetVaultExternalMigrationConfigs(); + + const { currentOrg } = useOrganization(); const { data: appConnections = [] } = useListAppConnections(); const { mutateAsync: deleteConfig } = useDeleteVaultExternalMigrationConfig(); @@ -157,7 +160,8 @@ export const VaultConnectionSection = () => { Configure namespace-specific connections to enable in-platform migration features. Manage connections in the{" "} App Connections diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx index e08d4d1b5..2a8e932a7 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx @@ -44,7 +44,11 @@ export const SubOrgNameChangeSection = (): JSX.Element => { subOrgId: currentOrg.id }); - navigate({ to: "/organization/settings", search: { subOrganization: name } }); + navigate({ + to: "/organizations/$orgId/settings", + params: { orgId: currentOrg.id }, + search: { subOrganization: name } + }); queryClient.invalidateQueries(); await router.invalidate({ sync: true }); createNotification({ diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPGroupMapModal.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPGroupMapModal.tsx index 31e96a1cb..9687c5d4d 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPGroupMapModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/LDAPGroupMapModal.tsx @@ -263,7 +263,8 @@ export const LDAPGroupMapModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: size="sm" onClick={() => navigate({ - to: "/organization/access-management" + to: "/organizations/$orgId/access-management", + params: { orgId: currentOrg.id } }) } > diff --git a/frontend/src/pages/organization/SettingsPage/route.tsx b/frontend/src/pages/organization/SettingsPage/route.tsx index ca104cf4f..28b71488a 100644 --- a/frontend/src/pages/organization/SettingsPage/route.tsx +++ b/frontend/src/pages/organization/SettingsPage/route.tsx @@ -9,7 +9,7 @@ const SettingsPageQueryParams = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/settings/" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/settings/" )({ component: SettingsPage, validateSearch: zodValidator(SettingsPageQueryParams), diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx index 2277e767d..608348029 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx @@ -91,7 +91,8 @@ const Page = withPermission( handlePopUpClose("removeMember"); navigate({ - to: "/organization/access-management" as const, + to: "/organizations/$orgId/access-management" as const, + params: { orgId }, search: { selectedTab: OrgAccessControlTabSections.Member } @@ -103,14 +104,15 @@ const Page = withPermission( {membership && (
- Users + Organization Users { const { data: workspaces = [] } = useGetUserProjects(); const navigate = useNavigate(); + const { currentOrg } = useOrganization(); const isAccessible = useMemo(() => { const workspaceIds = new Map(); @@ -46,6 +48,7 @@ export const UserProjectRow = ({ navigate({ to: `${getProjectBaseURL(project.type)}/access-management` as const, params: { + orgId: currentOrg?.id || "", projectId: project.id }, search: { diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/route.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/route.tsx index 4b733029b..7293066f8 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/route.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/route.tsx @@ -3,14 +3,14 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { UserDetailsByIDPage } from "./UserDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/members/$membershipId" )({ component: UserDetailsByIDPage, - context: () => ({ + context: ({ params }) => ({ breadcrumbs: [ { label: "Access Control", - link: linkOptions({ to: "/organization/access-management" }) + link: linkOptions({ to: "/organizations/$orgId/access-management" as const, params }) }, { label: "Users" diff --git a/frontend/src/pages/pam/PamAccountsPage/components/FolderBreadCrumbs.tsx b/frontend/src/pages/pam/PamAccountsPage/components/FolderBreadCrumbs.tsx index 726af0832..f32e3f682 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/FolderBreadCrumbs.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/FolderBreadCrumbs.tsx @@ -8,7 +8,7 @@ type Props = { export const FolderBreadCrumbs = ({ path = "/" }: Props) => { const navigate = useNavigate({ - from: "/projects/pam/$projectId/accounts" + from: "/organizations/$orgId/projects/pam/$projectId/accounts" }); const onFolderCrumbClick = (index: number) => { diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx index 704c13eba..8e9a0f8da 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx @@ -15,6 +15,10 @@ type Props = { }; export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) => { + const { protocol, hostname, port } = window.location; + const portSuffix = port && port !== "80" && port !== "443" ? `:${port}` : ""; + const siteURL = `${protocol}//${hostname}${portSuffix}`; + const [duration, setDuration] = useState("4h"); const isDurationValid = useMemo(() => duration && ms(duration || "1s") > 0, [duration]); @@ -64,9 +68,9 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) switch (account.resource.resourceType) { case PamResourceType.Postgres: case PamResourceType.MySQL: - return `infisical pam db access-account ${account.id} --duration ${cliDuration}`; + return `infisical pam db access-account ${account.id} --duration ${cliDuration} --domain ${siteURL}`; case PamResourceType.SSH: - return `infisical pam ssh access-account ${account.id} --duration ${cliDuration}`; + return `infisical pam ssh access-account ${account.id} --duration ${cliDuration} --domain ${siteURL}`; default: return ""; } diff --git a/frontend/src/pages/pam/PamAccountsPage/route.tsx b/frontend/src/pages/pam/PamAccountsPage/route.tsx index 65a8c567e..c9b9c68e4 100644 --- a/frontend/src/pages/pam/PamAccountsPage/route.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/route.tsx @@ -13,7 +13,7 @@ const PamAccountsPageQueryParamsSchema = z.object({ }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/accounts" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/accounts" )({ validateSearch: zodValidator(PamAccountsPageQueryParamsSchema), search: { @@ -27,7 +27,7 @@ export const Route = createFileRoute( { label: "Accounts", link: linkOptions({ - to: "/projects/pam/$projectId/accounts", + to: "/organizations/$orgId/projects/pam/$projectId/accounts", params: () => params as never, search: (prev) => ({ ...prev, accountPath: "/" }) }) @@ -37,7 +37,7 @@ export const Route = createFileRoute( return { label: segment, link: linkOptions({ - to: "/projects/pam/$projectId/accounts", + to: "/organizations/$orgId/projects/pam/$projectId/accounts", params: () => params as never, search: (prev) => ({ ...prev, accountPath: newPath }) }) diff --git a/frontend/src/pages/pam/PamResourcesPage/route.tsx b/frontend/src/pages/pam/PamResourcesPage/route.tsx index 37088289c..797d873c9 100644 --- a/frontend/src/pages/pam/PamResourcesPage/route.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/route.tsx @@ -5,7 +5,7 @@ import { z } from "zod"; import { PamResourcesPage } from "./PamResourcesPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/resources" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/resources" )({ validateSearch: zodValidator( z.object({ diff --git a/frontend/src/pages/pam/PamSessionsByIDPage/PamSessionByIDPage.tsx b/frontend/src/pages/pam/PamSessionsByIDPage/PamSessionByIDPage.tsx index eb6939e26..1c25dead3 100644 --- a/frontend/src/pages/pam/PamSessionsByIDPage/PamSessionByIDPage.tsx +++ b/frontend/src/pages/pam/PamSessionsByIDPage/PamSessionByIDPage.tsx @@ -6,7 +6,7 @@ import { Link, useParams } from "@tanstack/react-router"; import { ProjectPermissionCan } from "@app/components/permissions"; import { PageHeader } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { ProjectPermissionSub, useProject } from "@app/context"; +import { ProjectPermissionSub, useOrganization, useProject } from "@app/context"; import { ProjectPermissionPamSessionActions } from "@app/context/ProjectPermissionContext/types"; import { useGetPamSessionById } from "@app/hooks/api/pam"; import { ProjectType } from "@app/hooks/api/projects/types"; @@ -20,14 +20,16 @@ const Page = () => { select: (el) => el.sessionId }); const { data: session } = useGetPamSessionById(sessionId); + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); return (
{session && (
{ @@ -13,7 +13,7 @@ export const Route = createFileRoute( { label: "Sessions", link: linkOptions({ - to: "/projects/pam/$projectId/sessions", + to: "/organizations/$orgId/projects/pam/$projectId/sessions", params }) }, diff --git a/frontend/src/pages/pam/PamSessionsPage/route.tsx b/frontend/src/pages/pam/PamSessionsPage/route.tsx index ed88e9ad5..a97f92e8e 100644 --- a/frontend/src/pages/pam/PamSessionsPage/route.tsx +++ b/frontend/src/pages/pam/PamSessionsPage/route.tsx @@ -5,7 +5,7 @@ import { z } from "zod"; import { PamSessionPage } from "./PamSessionsPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/sessions/" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/" )({ validateSearch: zodValidator( z.object({ diff --git a/frontend/src/pages/pam/SettingsPage/SettingsPage.tsx b/frontend/src/pages/pam/SettingsPage/SettingsPage.tsx index fab7feb66..fec421a4f 100644 --- a/frontend/src/pages/pam/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/pam/SettingsPage/SettingsPage.tsx @@ -1,13 +1,18 @@ 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"; export const SettingsPage = () => { const { t } = useTranslation(); + const { currentOrg } = useOrganization(); + return (
@@ -16,9 +21,19 @@ export const SettingsPage = () => {
+ > + + Looking for organization settings? + + diff --git a/frontend/src/pages/pam/SettingsPage/route.tsx b/frontend/src/pages/pam/SettingsPage/route.tsx index aef975a2f..cf90be4d3 100644 --- a/frontend/src/pages/pam/SettingsPage/route.tsx +++ b/frontend/src/pages/pam/SettingsPage/route.tsx @@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { SettingsPage } from "./SettingsPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/settings" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/settings" )({ component: SettingsPage, beforeLoad: ({ context }) => { diff --git a/frontend/src/pages/pam/layout.tsx b/frontend/src/pages/pam/layout.tsx index 9d2ee2a60..864365bea 100644 --- a/frontend/src/pages/pam/layout.tsx +++ b/frontend/src/pages/pam/layout.tsx @@ -8,7 +8,7 @@ import { PamLayout } from "@app/layouts/PamLayout"; import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout" )({ component: PamLayout, beforeLoad: async ({ params, context }) => { diff --git a/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx b/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx index aaa3a6eb7..83624c98d 100644 --- a/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx +++ b/frontend/src/pages/project/AccessControlPage/AccessControlPage.tsx @@ -1,9 +1,10 @@ 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 { useProject } from "@app/context"; +import { useOrganization, useProject } from "@app/context"; import { getProjectBaseURL } from "@app/helpers/project"; import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectAccessControlTabs } from "@app/types/project"; @@ -18,6 +19,7 @@ import { const Page = () => { const navigate = useNavigate(); + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const selectedTab = useSearch({ strict: false, @@ -29,6 +31,7 @@ const Page = () => { to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, search: (prev) => ({ ...prev, selectedTab: tab }), params: { + orgId: currentOrg.id, projectId: currentProject.id } }); @@ -41,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? + + @@ -53,7 +66,7 @@ const Page = () => { Groups - Identities + Machine Identities {isSecretManager && ( diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx index f8788762c..17ef73120 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx @@ -140,7 +140,7 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => {
All groups in your organization have already been added to this project.
- +
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 3ce5b4062..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 @@ -32,7 +32,12 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useOrganization, + useProject +} from "@app/context"; import { getProjectBaseURL } from "@app/helpers/project"; import { getUserTablePreference, @@ -61,6 +66,7 @@ enum GroupsOrderBy { } export const GroupTable = ({ handlePopUpOpen }: Props) => { + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const navigate = useNavigate(); @@ -116,7 +122,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { value={search} onChange={(e) => setSearch(e.target.value)} leftIcon={} - placeholder="Search members..." + placeholder="Search project groups..." /> @@ -137,7 +143,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { - + @@ -161,6 +167,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { navigate({ to: `${getProjectBaseURL(currentProject.type)}/groups/$groupId` as const, params: { + orgId: currentOrg.id, projectId: currentProject.id, groupId: id } @@ -171,6 +178,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { navigate({ to: `${getProjectBaseURL(currentProject.type)}/groups/$groupId` as const, params: { + orgId: currentOrg.id, projectId: currentProject.id, groupId: id } diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx index e98633368..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( - + @@ -277,6 +277,7 @@ export const IdentityTab = withProjectPermission( navigate({ to: `${getProjectBaseURL(currentProject.type)}/identities/$identityId` as const, params: { + orgId: currentOrg.id, projectId: currentProject.id, identityId: id } @@ -287,6 +288,7 @@ export const IdentityTab = withProjectPermission( navigate({ to: `${getProjectBaseURL(currentProject.type)}/identities/$identityId` as const, params: { + orgId: currentOrg.id, projectId: currentProject.id, identityId: id } @@ -452,7 +454,9 @@ export const IdentityTab = withProjectPermission( }); }} > - {identityProjectId ? "Delete Identity" : "Remove From Project"} + {identityProjectId + ? "Delete Machine Identity" + : "Remove From Project"} )} @@ -472,7 +476,7 @@ export const IdentityTab = withProjectPermission( @@ -495,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} /> @@ -506,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) => ( )} @@ -215,7 +219,7 @@ const Page = () => { {!isProjectIdentity && ( - This identity is managed by your organization.{" "} + This machine identity is managed by your organization.{" "} { {(isAllowed) => isAllowed ? ( - Click here to manage identity. + Click here to manage machine identity. ) : null @@ -281,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/IdentityDetailsByIDPage/route-cert-manager.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-cert-manager.tsx index d6ef9aa73..934674252 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/route-cert-manager.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-cert-manager.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/identities/$identityId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/identities/$identityId" )({ component: IdentityDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/cert-management/$projectId/access-management", + to: "/organizations/$orgId/projects/cert-management/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/route-kms.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-kms.tsx index f4fb109e3..8fa4a7f89 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/route-kms.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-kms.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/identities/$identityId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/kms/$projectId/_kms-layout/identities/$identityId" )({ component: IdentityDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/kms/$projectId/access-management", + to: "/organizations/$orgId/projects/kms/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/route-pam.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-pam.tsx index ea780d952..0b94861a7 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/route-pam.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-pam.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/identities/$identityId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/identities/$identityId" )({ component: IdentityDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/pam/$projectId/access-management", + to: "/organizations/$orgId/projects/pam/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-manager.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-manager.tsx index 355428940..8fb379b6e 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-manager.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-manager.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/identities/$identityId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/identities/$identityId" )({ component: IdentityDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/secret-management/$projectId/access-management", + to: "/organizations/$orgId/projects/secret-management/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-scanning.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-scanning.tsx index c5491bf72..8b657d649 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-scanning.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-secret-scanning.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId" )({ component: IdentityDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/secret-scanning/$projectId/access-management", + to: "/organizations/$orgId/projects/secret-scanning/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx index 2a82c371f..8a4df2b49 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/identities/$identityId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ssh/$projectId/_ssh-layout/identities/$identityId" )({ component: IdentityDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/ssh/$projectId/access-management", + to: "/organizations/$orgId/projects/ssh/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx index 2c578ea4f..62b4e4c43 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx @@ -74,7 +74,9 @@ export const Page = () => { }); const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization ? `?subOrganization=${currentOrg.slug}` : ""}`; - window.location.href = url.replace("$projectId", currentProject.id); + window.location.assign( + url.replace("$orgId", currentOrg.id).replace("$projectId", currentProject.id) + ); } } ); @@ -95,7 +97,8 @@ export const Page = () => { navigate({ to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, params: { - projectId: currentProject.id + projectId: currentProject.id, + orgId: currentOrg.id } }); handlePopUpClose("removeMember"); @@ -116,7 +119,8 @@ export const Page = () => { { className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" > - Users + Project Users { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/cert-management/$projectId/access-management", + to: "/organizations/$orgId/projects/cert-management/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/route-kms.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/route-kms.tsx index 6628de005..84ccd2501 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/route-kms.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/route-kms.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/members/$membershipId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/kms/$projectId/_kms-layout/members/$membershipId" )({ component: MemberDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/kms/$projectId/access-management", + to: "/organizations/$orgId/projects/kms/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/route-pam.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/route-pam.tsx index bf992e6ed..565506244 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/route-pam.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/route-pam.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/members/$membershipId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/members/$membershipId" )({ component: MemberDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/pam/$projectId/access-management", + to: "/organizations/$orgId/projects/pam/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-manager.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-manager.tsx index 2114e5b40..72d60e777 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-manager.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-manager.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/members/$membershipId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/members/$membershipId" )({ component: MemberDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/secret-management/$projectId/access-management", + to: "/organizations/$orgId/projects/secret-management/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-scanning.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-scanning.tsx index ff66305fd..d20d4acfe 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-scanning.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/route-secret-scanning.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/members/$membershipId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/members/$membershipId" )({ component: MemberDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/secret-scanning/$projectId/access-management", + to: "/organizations/$orgId/projects/secret-scanning/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx index 577c84e3f..fc9383b64 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/members/$membershipId" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ssh/$projectId/_ssh-layout/members/$membershipId" )({ component: MemberDetailsByIDPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/ssh/$projectId/access-management", + to: "/organizations/$orgId/projects/ssh/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx index 9a87266c4..e6311d081 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/RoleDetailsBySlugPage.tsx @@ -22,7 +22,12 @@ import { DropdownMenuTrigger, PageHeader } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useOrganization, + useProject +} from "@app/context"; import { getProjectBaseURL } from "@app/helpers/project"; import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; @@ -40,7 +45,9 @@ const Page = () => { select: (el) => el.roleSlug as string }); const { currentProject } = useProject(); + const { currentOrg } = useOrganization(); const projectId = currentProject?.id || ""; + const orgId = currentOrg?.id || ""; const { data } = useGetProjectRoleBySlug(projectId, roleSlug as string); @@ -68,7 +75,8 @@ const Page = () => { navigate({ to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, params: { - projectId + projectId, + orgId }, search: { selectedTab: ProjectAccessControlTabs.Roles @@ -87,7 +95,8 @@ const Page = () => { { className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" > - Roles + Project Roles { resolver: zodResolver(schema) }); + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const createRole = useCreateProjectRole(); @@ -83,6 +84,7 @@ const Content = ({ role, onClose }: ContentProps) => { navigate({ to: `${getProjectBaseURL(currentProject.type)}/roles/$roleSlug` as const, params: { + orgId: currentOrg.id, roleSlug: newRole.slug, projectId: currentProject.id } diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx index 63853ad51..3505722bc 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal.tsx @@ -18,7 +18,8 @@ import { Tooltip, Tr } from "@app/components/v2"; -import { ProjectPermissionSub } from "@app/context"; +import { ProjectPermissionSub, useProject } from "@app/context"; +import { useGetWorkspaceIntegrations } from "@app/hooks/api"; import { ProjectType } from "@app/hooks/api/projects/types"; import { @@ -46,6 +47,9 @@ type TForm = { permissions: Record }; 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/RoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx index 03525eac5..4fcb246ef 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; -import { useProject } from "@app/context"; +import { useOrganization, useProject } from "@app/context"; import { getProjectBaseURL } from "@app/helpers/project"; import { useCreateProjectRole, @@ -38,6 +38,9 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { roleSlug: string; }; + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { currentProject } = useProject(); const projectId = currentProject?.id || ""; @@ -93,6 +96,7 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { navigate({ to: `${getProjectBaseURL(currentProject.type)}/roles/$roleSlug` as const, params: { + orgId, roleSlug: slug, projectId } @@ -111,6 +115,7 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { navigate({ to: `${getProjectBaseURL(currentProject.type)}/roles/$roleSlug` as const, params: { + orgId, roleSlug: newRole.slug, projectId } 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) => ( { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/cert-management/$projectId/access-management", + to: "/organizations/$orgId/projects/cert-management/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/route-kms.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/route-kms.tsx index e1eae2756..fe578bca8 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/route-kms.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/route-kms.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/roles/$roleSlug" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/kms/$projectId/_kms-layout/roles/$roleSlug" )({ component: RoleDetailsBySlugPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/kms/$projectId/access-management", + to: "/organizations/$orgId/projects/kms/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/route-pam.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/route-pam.tsx index 3afd5963c..7ed13f360 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/route-pam.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/route-pam.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/pam/$projectId/_pam-layout/roles/$roleSlug" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/roles/$roleSlug" )({ component: RoleDetailsBySlugPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/pam/$projectId/access-management", + to: "/organizations/$orgId/projects/pam/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-manager.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-manager.tsx index ad4d44d67..fc93bf315 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-manager.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-manager.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/roles/$roleSlug" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/roles/$roleSlug" )({ component: RoleDetailsBySlugPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/secret-management/$projectId/access-management", + to: "/organizations/$orgId/projects/secret-management/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-scanning.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-scanning.tsx index 6d5ad64e5..9a8c118c8 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-scanning.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/route-secret-scanning.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/secret-scanning/$projectId/_secret-scanning-layout/roles/$roleSlug" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-scanning/$projectId/_secret-scanning-layout/roles/$roleSlug" )({ component: RoleDetailsBySlugPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/secret-scanning/$projectId/access-management", + to: "/organizations/$orgId/projects/secret-scanning/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx index 1f06f795d..6861dd03d 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx @@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project"; import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/projects/ssh/$projectId/_ssh-layout/roles/$roleSlug" + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/ssh/$projectId/_ssh-layout/roles/$roleSlug" )({ component: RoleDetailsBySlugPage, beforeLoad: ({ context, params }) => { @@ -15,8 +15,9 @@ export const Route = createFileRoute( { label: "Access Control", link: linkOptions({ - to: "/projects/ssh/$projectId/access-management", + to: "/organizations/$orgId/projects/ssh/$projectId/access-management", params: { + orgId: params.orgId, projectId: params.projectId }, search: { diff --git a/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx b/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx index 58db07484..8e3290563 100644 --- a/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx +++ b/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx @@ -65,7 +65,8 @@ export const DeleteProjectSection = () => { }); navigate({ - to: "/organization/projects" + to: "/organizations/$orgId/projects", + params: { orgId: currentOrg.id } }); handlePopUpClose("deleteWorkspace"); } finally { @@ -110,7 +111,8 @@ export const DeleteProjectSection = () => { }); navigate({ - to: "/organization/projects" + to: "/organizations/$orgId/projects", + params: { orgId: currentOrg.id } }); } finally { setIsLeaving.off(); diff --git a/frontend/src/pages/public/ErrorPage/ErrorPage.tsx b/frontend/src/pages/public/ErrorPage/ErrorPage.tsx index f865d2940..4bc74b6c3 100644 --- a/frontend/src/pages/public/ErrorPage/ErrorPage.tsx +++ b/frontend/src/pages/public/ErrorPage/ErrorPage.tsx @@ -41,7 +41,7 @@ export const ErrorPage = ({ error }: ErrorComponentProps) => { {" "} if the issue persists.

- + - )} -
-
-

{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: { - 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/IntegrationRow.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx index c966cda56..548472cf5 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationRow.tsx @@ -16,7 +16,12 @@ import { twMerge } from "tailwind-merge"; import { ProjectPermissionCan } from "@app/components/permissions"; import { IconButton, Td, Tooltip, Tr } from "@app/components/v2"; import { Badge } from "@app/components/v3"; -import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useOrganization, + useProject +} from "@app/context"; import { TCloudIntegration } from "@app/hooks/api/integrations/types"; import { TIntegration } from "@app/hooks/api/types"; @@ -38,6 +43,7 @@ export const IntegrationRow = ({ cloudIntegration }: IProps) => { const navigate = useNavigate(); + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const { id, secretPath, syncMessage, isSynced } = integration; @@ -60,8 +66,9 @@ export const IntegrationRow = ({