Merge branch 'main' into feature/mongodb-secret-rotation

This commit is contained in:
Victor Santos
2025-12-03 17:23:06 -03:00
877 changed files with 20380 additions and 10351 deletions
+2 -13
View File
@@ -31,25 +31,14 @@ SMTP_FROM_NAME=
SMTP_USERNAME= SMTP_USERNAME=
SMTP_PASSWORD= SMTP_PASSWORD=
# Integration # CICD Integration
# Optional only if integration is used
CLIENT_ID_HEROKU=
CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB= CLIENT_ID_GITHUB=
CLIENT_ID_GITHUB_APP= CLIENT_ID_GITHUB_APP=
CLIENT_SLUG_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=
CLIENT_SECRET_GITHUB_APP= CLIENT_SECRET_GITHUB_APP=
CLIENT_ID_GITLAB=
CLIENT_SECRET_GITLAB= CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL=
CLIENT_PRIVATE_KEY_GITHUB_APP= CLIENT_PRIVATE_KEY_GITHUB_APP=
CLIENT_APP_ID_GITHUB_APP= CLIENT_APP_ID_GITHUB_APP=
+19 -17
View File
@@ -1,23 +1,25 @@
# Description 📣 ## Context
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. Here's how we expect a pull request to be : https://infisical.com/docs/contributing/getting-started/pull-requests --> <!-- What problem does this solve? What was the behavior before, and what is it now? Add all relevant context. Link related issues/tickets. -->
## Type ✨ ## Screenshots
- [ ] Bug fix <!-- If UI/UX changes, add screenshots or videos. Delete if not applicable. -->
- [ ] New feature
## Steps to verify the change
## Type
- [ ] Fix
- [ ] Feature
- [ ] Improvement - [ ] Improvement
- [ ] Breaking change - [ ] Breaking
- [ ] Documentation - [ ] Docs
- [ ] Chore
# Tests 🛠️ ## Checklist
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible --> - [ ] 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
```sh - [ ] Updated docs (if needed)
# Here's some code block to paste some code snippets - [ ] Read the [contributing guide](https://infisical.com/docs/contributing/getting-started/overview)
```
---
- [ ] 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). 📝
+55
View File
@@ -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}"`);
}
+1
View File
@@ -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/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/aws-secrets-manager.mdx:private-key:62
docs/documentation/platform/pki/certificate-syncs/chef.mdx:private-key:61 docs/documentation/platform/pki/certificate-syncs/chef.mdx:private-key:61
backend/src/services/certificate-request/certificate-request-service.test.ts:private-key:246
+3
View File
@@ -185,6 +185,9 @@ COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app ./backend/frontend-build 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 ARG INFISICAL_PLATFORM_VERSION
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
+4
View File
@@ -34,6 +34,7 @@ ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID ARG INTERCOM_ID
ENV VITE_INTERCOM_ID $INTERCOM_ID ENV VITE_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION ARG INFISICAL_PLATFORM_VERSION
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG CAPTCHA_SITE_KEY ARG CAPTCHA_SITE_KEY
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
@@ -173,6 +174,9 @@ ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
COPY --from=backend-runner /app /backend COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app ./backend/frontend-build 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 ARG INFISICAL_PLATFORM_VERSION
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
+2 -3
View File
@@ -87,14 +87,13 @@ def bootstrap_infisical(context: Context):
ca_slug = faker.slug() ca_slug = faker.slug()
resp = client.post( resp = client.post(
"/api/v1/pki/ca/internal", "/api/v1/cert-manager/ca/internal",
headers=headers, headers=headers,
json={ json={
"projectId": project["id"], "projectId": project["id"],
"name": ca_slug, "name": ca_slug,
"type": "internal", "type": "internal",
"status": "active", "status": "active",
"enableDirectIssuance": True,
"configuration": { "configuration": {
"type": "root", "type": "root",
"organization": "Infisican Inc", "organization": "Infisican Inc",
@@ -115,7 +114,7 @@ def bootstrap_infisical(context: Context):
cert_template_slug = faker.slug() cert_template_slug = faker.slug()
resp = client.post( resp = client.post(
"/api/v2/certificate-templates", "/api/v1/cert-manager/certificate-templates",
headers=headers, headers=headers,
json={ json={
"projectId": project["id"], "projectId": project["id"],
@@ -2,7 +2,7 @@ Feature: Access Control
Scenario Outline: Access resources across different account Scenario Outline: Access resources across different account
Given I have an ACME cert profile as "acme_profile" 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 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>[^/]+)$") | .id" as account0_id Then I memorize acme_account0.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account0_id
When I create certificate signing request as csr 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 Then the value response.status_code should not be equal to 404
And I put away current ACME client as client0 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 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 Then I peak and memorize the next nonce as nonce
When I send a raw ACME request to "<url>" When I send a raw ACME request to "<url>"
@@ -53,7 +53,7 @@ Feature: Access Control
Examples: Endpoints Examples: Endpoints
| src_var | jq | dest_var | url | payload | | 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} | |
| order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} | | order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} |
| order | . | not_used | {order.uri}/certificate | | | order | . | not_used | {order.uri}/certificate | |
@@ -62,7 +62,7 @@ Feature: Access Control
Scenario Outline: Access resources across a different profiles Scenario Outline: Access resources across a different profiles
Given I have an ACME cert profile as "acme_profile" 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 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>[^/]+)$") | .id" as account0_id Then I memorize acme_account0.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account0_id
When I create certificate signing request as csr 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 make a random slug as profile_slug
Given I use AUTH_TOKEN for authentication 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}", "projectId": "{PROJECT_ID}",
@@ -110,10 +110,10 @@ Feature: Access Control
""" """
Then the value response.status_code should be equal to 200 Then the value response.status_code should be equal to 200
Then I memorize response with jq ".certificateProfile.id" as profile_id 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 Then I memorize response with jq ".eabKid" as eab_kid
And I memorize response with jq ".eabSecret" as eab_secret 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 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 peak and memorize the next nonce as nonce
Then I memorize <src_var> with jq "<jq>" as <dest_var> Then I memorize <src_var> with jq "<jq>" as <dest_var>
@@ -133,7 +133,7 @@ Feature: Access Control
Examples: Endpoints Examples: Endpoints
| src_var | jq | dest_var | url | payload | | 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} | |
| order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} | | order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} |
| order | . | not_used | {order.uri}/certificate | | | 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 Scenario Outline: Access resources across a different profile with the same key pair
Given I have an ACME cert profile as "acme_profile" 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 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>[^/]+)$") | .id" as account0_id Then I memorize acme_account0.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account0_id
When I create certificate signing request as csr 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 make a random slug as profile_slug
Given I use AUTH_TOKEN for authentication 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}", "projectId": "{PROJECT_ID}",
@@ -191,10 +191,10 @@ Feature: Access Control
""" """
Then the value response.status_code should be equal to 200 Then the value response.status_code should be equal to 200
Then I memorize response with jq ".certificateProfile.id" as profile_id 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 Then I memorize response with jq ".eabKid" as eab_kid
And I memorize response with jq ".eabSecret" as eab_secret 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 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 peak and memorize the next nonce as nonce
Then I memorize <src_var> with jq "<jq>" as <dest_var> Then I memorize <src_var> with jq "<jq>" as <dest_var>
@@ -214,7 +214,7 @@ Feature: Access Control
Examples: Endpoints Examples: Endpoints
| src_var | jq | dest_var | url | payload | | 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} | |
| order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} | | order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} |
| order | . | not_used | {order.uri}/certificate | | | order | . | not_used | {order.uri}/certificate | |
@@ -223,7 +223,7 @@ Feature: Access Control
Scenario Outline: URL mismatch Scenario Outline: URL mismatch
Given I have an ACME cert profile as "acme_profile" 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 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>[^/]+)$") | .id" as account_id Then I memorize acme_account.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account_id
When I create certificate signing request as csr When I create certificate signing request as csr
@@ -258,8 +258,8 @@ Feature: Access Control
Examples: Endpoints Examples: Endpoints
| src_var | jq | dest_var | actual_url | bad_url | error_detail | | 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/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/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 | 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} | 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} | 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 | | 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 Scenario Outline: Send KID and JWK in the same time
Given I have an ACME cert profile as "acme_profile" 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 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>[^/]+)$") | .id" as account_id And I memorize acme_account.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account_id
When I create certificate signing request as csr When I create certificate signing request as csr
@@ -312,8 +312,8 @@ Feature: Access Control
Examples: Endpoints Examples: Endpoints
| src_var | jq | dest_var | url | | 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/cert-manager/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}/new-order |
| order | . | not_used | {order.uri} | | order | . | not_used | {order.uri} |
| order | . | not_used | {order.uri}/finalize | | order | . | not_used | {order.uri}/finalize |
| order | . | not_used | {order.uri}/certificate | | order | . | not_used | {order.uri}/certificate |
+15 -15
View File
@@ -2,13 +2,13 @@ Feature: Account
Scenario: Create a new account Scenario: Create a new account
Given I have an ACME cert profile as "acme_profile" 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 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 Scenario: Create a new account with the same key pair twice
Given I have an ACME cert profile as "acme_profile" 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 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 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 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 Scenario: Find an existing account
Given I have an ACME cert profile as "acme_profile" 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 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 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 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. # Note: This is a very special case for cert-manager.
Scenario: Create a new account with EAB then retrieve it without EAB Scenario: Create a new account with EAB then retrieve it without EAB
Given I have an ACME cert profile as "acme_profile" 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 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 memorize acme_account.uri as account_uri
And I find the existing ACME account without EAB as retrieved_account 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 Scenario: Create a new account without EAB
Given I have an ACME cert profile as "acme_profile" 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 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" 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 Scenario Outline: Scenario: Create a new account with bad EAB credentials
Given I have an ACME cert profile as "acme_profile" 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 "<eab_kid>" with secret "<eab_secret>" as acme_account 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
And the value error with jq ".type" should be equal to "<error_type>" And the value error with jq ".type" should be equal to "<error_type>"
And the value error with jq ".detail" should be equal to "<error_msg>" And the value error with jq ".detail" should be equal to "<error_msg>"
@@ -57,17 +57,17 @@ Feature: Account
Scenario Outline: Scenario: Create a new account with bad EAB url Scenario Outline: Scenario: Create a new account with bad EAB url
Given I have an ACME cert profile as "acme_profile" 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 "<url>" for EAB signature And I use a different new-account URL "<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 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 ".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" And the value error with jq ".detail" should be equal to "External account binding URL mismatch"
Examples: Bad URLs Examples: Bad URLs
| url | | url |
| {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account-bad | | {BASE_URL}/api/v1/cert-manager/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/cert-manager/acme/profiles/{acme_profile.id}/new-account?foo=bar |
| {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account#foobar | | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account#foobar |
| {BASE_URL}/acme/new-account | | {BASE_URL}/acme/new-account |
| https://example.com/api/v1/pki/acme/profiles/{acme_profile.id}/new-account-bad | | https://example.com/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account-bad |
| bad | | bad |
+2 -2
View File
@@ -2,7 +2,7 @@ Feature: Authorization
Scenario: Get authorization Scenario: Get authorization
Given I have an ACME cert profile as "acme_profile" 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 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 When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -14,7 +14,7 @@ Feature: Authorization
Then I create a RSA private key pair as cert_key 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 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 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 ".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 And the value order.authorizations[0].body with jq ".challenges | map(pick(.type, .status)) | sort_by(.type)" should be equal to json
""" """
@@ -3,7 +3,7 @@ Feature: ACME Cert Profile
Scenario: Create a cert profile Scenario: Create a cert profile
Given I make a random slug as profile_slug Given I make a random slug as profile_slug
And I use AUTH_TOKEN for authentication 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}", "projectId": "{PROJECT_ID}",
@@ -25,7 +25,7 @@ Feature: ACME Cert Profile
Scenario: Reveal EAB secret Scenario: Reveal EAB secret
Given I make a random slug as profile_slug Given I make a random slug as profile_slug
And I use AUTH_TOKEN for authentication 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}", "projectId": "{PROJECT_ID}",
@@ -39,11 +39,11 @@ Feature: ACME Cert Profile
""" """
Then the value response.status_code should be equal to 200 Then the value response.status_code should be equal to 200
And I memorize response with jq ".certificateProfile.id" as profile_id 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 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 ".eabKid" should be equal to "{profile_id}"
And the value response with jq ".eabSecret" should be present 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 ".eabKid" as eab_kid
And I memorize response with jq ".eabSecret" as eab_secret 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 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
+38 -52
View File
@@ -2,7 +2,7 @@ Feature: Challenge
Scenario: Validate challenge Scenario: Validate challenge
Given I have an ACME cert profile as "acme_profile" 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 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 When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -22,9 +22,31 @@ Feature: Challenge
And I parse the full-chain certificate from order finalized_order as cert 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" 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 Scenario: Validate challenges for multiple domains
Given I have an ACME cert profile as "acme_profile" 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 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 When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -58,18 +80,17 @@ Feature: Challenge
Scenario: Did not finish all challenges Scenario: Did not finish all challenges
Given I have an ACME cert profile as "acme_profile" 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 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 When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
""" """
{ {}
"COMMON_NAME": "localhost"
}
""" """
And I add subject alternative name to certificate signing request csr And I add subject alternative name to certificate signing request csr
""" """
[ [
"localhost",
"infisical.com" "infisical.com"
] ]
""" """
@@ -82,56 +103,19 @@ Feature: Challenge
# the localhost auth should be valid # 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 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 And I wait until the status of authorization localhost_auth becomes valid
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"
# the infisical.com auth should still be pending # 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 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 And I post-as-get {infisical_auth} as infisical_auth_resp
When I send a raw ACME request to "{infisical_auth}" And the value infisical_auth_resp with jq ".status" should be equal to "pending"
"""
{
"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"
# the order should be pending as well # the order should be pending as well
And I memorize response.headers with jq ".["replay-nonce"]" as nonce And I post-as-get {order.uri} as order_resp
When I send a raw ACME request to "{order.uri}" And the value order_resp with jq ".status" should be equal to "pending"
"""
{
"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"
# finalize should not be allowed when all auths are not valid yet # 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}" When I send a raw ACME request to "{order.body.finalize}"
""" """
{ {
@@ -153,7 +137,7 @@ Feature: Challenge
Scenario: CSR names mismatch with order identifier Scenario: CSR names mismatch with order identifier
Given I have an ACME cert profile as "acme_profile" 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 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 When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -165,13 +149,13 @@ Feature: Challenge
And I create a RSA private key pair as cert_key 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 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 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": { "protected": {
"alg": "RS256", "alg": "RS256",
"nonce": "{nonce}", "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}" "kid": "{acme_account.uri}"
}, },
"payload": { "payload": {
@@ -185,8 +169,10 @@ Feature: Challenge
Then the value response.status_code should be equal to 201 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 with jq ".finalize" as finalize_url
And I memorize response.headers with jq ".["replay-nonce"]" as nonce 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 memorize response as order
And I pass all challenges with type http-01 for order in 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 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}" When I send a raw ACME request to "{finalize_url}"
""" """
@@ -2,14 +2,14 @@ Feature: Directory
Scenario: Get the directory of ACME service urls Scenario: Get the directory of ACME service urls
Given I have an ACME cert profile as "acme_profile" 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" Then the response status code should be "200"
And the response body should match JSON value And the response body should match JSON value
""" """
{ {
"newNonce": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce", "newNonce": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-nonce",
"newAccount": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account", "newAccount": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account",
"newOrder": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order", "newOrder": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order",
"meta": { "meta": {
"externalAccountRequired": true "externalAccountRequired": true
} }
@@ -1,6 +1,7 @@
Feature: External CA Feature: External CA
Scenario: Issue a certificate from an external CA @cloudflare
Scenario Outline: Issue a certificate from an external CA with Cloudflare
Given I create a Cloudflare connection as cloudflare Given I create a Cloudflare connection as cloudflare
Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id
Given I create a external ACME CA with the following config as ext_ca Given I create a external ACME CA with the following config as ext_ca
@@ -87,14 +88,12 @@ Feature: External CA
""" """
Then I memorize cert_template with jq ".certificateTemplate.id" as cert_template_id 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" 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 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 When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
""" """
{ <subject>
"COMMON_NAME": "localhost"
}
""" """
# Pebble has a strict rule to only takes SANs # Pebble has a strict rule to only takes SANs
Then I add subject alternative name to certificate signing request csr Then I add subject alternative name to certificate signing request csr
@@ -177,4 +176,542 @@ Feature: External CA
[ [
"localhost" "localhost"
] ]
""" """
Examples:
| subject |
| {"COMMON_NAME": "localhost"} |
| {} |
@dnsme
Scenario Outline: Issue a certificate from an external CA with DNS Made Easy
Given I create a DNS Made Easy connection as dnsme
Then I memorize dnsme with jq ".appConnection.id" as app_conn_id
Given I create a external ACME CA with the following config as ext_ca
"""
{
"dnsProviderConfig": {
"provider": "dns-made-easy",
"hostedZoneId": "MOCK_ZONE_ID"
},
"directoryUrl": "{PEBBLE_URL}",
"accountEmail": "fangpen@infisical.com",
"dnsAppConnectionId": "{app_conn_id}",
"eabKid": "",
"eabHmacKey": ""
}
"""
Then I memorize ext_ca with jq ".id" as ext_ca_id
Given I create a certificate template with the following config as cert_template
"""
{
"subject": [
{
"type": "common_name",
"allowed": [
"*"
]
}
],
"sans": [
{
"type": "dns_name",
"allowed": [
"*"
]
}
],
"keyUsages": {
"required": [],
"allowed": [
"digital_signature",
"key_encipherment",
"non_repudiation",
"data_encipherment",
"key_agreement",
"key_cert_sign",
"crl_sign",
"encipher_only",
"decipher_only"
]
},
"extendedKeyUsages": {
"required": [],
"allowed": [
"client_auth",
"server_auth",
"code_signing",
"email_protection",
"ocsp_signing",
"time_stamping"
]
},
"algorithms": {
"signature": [
"SHA256-RSA",
"SHA512-RSA",
"SHA384-ECDSA",
"SHA384-RSA",
"SHA256-ECDSA",
"SHA512-ECDSA"
],
"keyAlgorithm": [
"RSA-2048",
"RSA-4096",
"ECDSA-P384",
"RSA-3072",
"ECDSA-P256",
"ECDSA-P521"
]
},
"validity": {
"max": "365d"
}
}
"""
Then I memorize cert_template with jq ".certificateTemplate.id" as cert_template_id
Given I create an ACME profile with ca {ext_ca_id} and template {cert_template_id} as "acme_profile"
When I have an ACME client connecting to "{BASE_URL}/api/v1/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
"""
<subject>
"""
# Pebble has a strict rule to only takes SANs
Then I add subject alternative name to certificate signing request csr
"""
[
"localhost"
]
"""
And I create a RSA private key pair as cert_key
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
And I select challenge with type http-01 for domain localhost from order in order as challenge
And I serve challenge response for challenge at localhost
And I tell ACME server that challenge is ready to be verified
Given I intercept outgoing requests
"""
[
{
"scope": "https://api.dnsmadeeasy.com:443",
"method": "POST",
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records",
"status": 201,
"response": {
"gtdLocation": "DEFAULT",
"failed": false,
"monitor": false,
"failover": false,
"sourceId": 895364,
"dynamicDns": false,
"hardLink": false,
"ttl": 60,
"source": 1,
"name": "_acme-challenge",
"value": "\"MOCK_HTTP_01_VALUE\"",
"id": 12345678,
"type": "TXT"
},
"responseIsBinary": false
},
{
"scope": "https://api.dnsmadeeasy.com:443",
"method": "GET",
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records?type=TXT&recordName=_acme-challenge&page=0",
"status": 200,
"response": {
"totalRecords": 1,
"totalPages": 1,
"data": [
{
"gtdLocation": "DEFAULT",
"failed": false,
"monitor": false,
"failover": false,
"sourceId": 895364,
"dynamicDns": false,
"hardLink": false,
"ttl": 60,
"source": 1,
"name": "_acme-challenge",
"value": "\"MOCK_CHALLENGE_VALUE\"",
"id": 1111111,
"type": "TXT"
}
],
"page": 0
},
"responseIsBinary": false
},
{
"scope": "https://api.dnsmadeeasy.com:443",
"method": "DELETE",
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records/1111111",
"status": 200,
"response": "",
"responseIsBinary": false
}
]
"""
Then I poll and finalize the ACME order order as finalized_order
And the value finalized_order.body with jq ".status" should be equal to "valid"
And I parse the full-chain certificate from order finalized_order as cert
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
"""
[
"localhost"
]
"""
Examples:
| subject |
| {"COMMON_NAME": "localhost"} |
| {} |
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
"""
<subject>
"""
Then I add subject alternative name to certificate signing request csr
"""
<san>
"""
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 "<err_detail>"
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": [
"<allowed_signature>"
],
"keyAlgorithm": [
"<allowed_alg>"
]
},
"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 <key_type> private key pair as cert_key
And I sign the certificate signing request csr with "<hash_type>" 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 "<err_detail>"
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 |
@@ -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"
]
"""
+21 -21
View File
@@ -2,13 +2,13 @@ Feature: Nonce
Scenario: Generate a new nonce Scenario: Generate a new nonce
Given I have an ACME cert profile as "acme_profile" 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" Then the response status code should be "200"
And the response header "Replay-Nonce" should contains non-empty value And the response header "Replay-Nonce" should contains non-empty value
Scenario Outline: Send a bad nonce to account endpoints Scenario Outline: Send a bad nonce to account endpoints
Given I have an ACME cert profile as "acme_profile" 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 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>[^/]+)$") | .id" as account_id And I memorize acme_account.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account_id
When I create certificate signing request as csr 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" And the value response with jq ".detail" should be equal to "Invalid nonce"
Examples: Endpoints Examples: Endpoints
| src_var | jq | dest_var | url | | 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/cert-manager/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}/new-order |
| order | . | not_used | {order.uri} | | order | . | not_used | {order.uri} |
| order | . | not_used | {order.uri}/finalize | | order | . | not_used | {order.uri}/finalize |
| order | . | not_used | {order.uri}/certificate | | order | . | not_used | {order.uri}/certificate |
| order | .authorizations[0].uri | auth_uri | {auth_uri} | | order | .authorizations[0].uri | auth_uri | {auth_uri} |
| order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} |
Scenario Outline: Send the same nonce twice Scenario Outline: Send the same nonce twice
Given I have an ACME cert profile as "acme_profile" 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 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>[^/]+)$") | .id" as account_id And I memorize acme_account.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account_id
When I create certificate signing request as csr 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 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 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 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": { "protected": {
"alg": "RS256", "alg": "RS256",
"nonce": "{nonce_value}", "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}" "kid": "{acme_account.uri}"
}, },
"payload": {} "payload": {}
@@ -97,11 +97,11 @@ Feature: Nonce
And the value response with jq ".detail" should be equal to "Invalid nonce" And the value response with jq ".detail" should be equal to "Invalid nonce"
Examples: Endpoints Examples: Endpoints
| src_var | jq | dest_var | url | | 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/cert-manager/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}/new-order |
| order | . | not_used | {order.uri} | | order | . | not_used | {order.uri} |
| order | . | not_used | {order.uri}/finalize | | order | . | not_used | {order.uri}/finalize |
| order | . | not_used | {order.uri}/certificate | | order | . | not_used | {order.uri}/certificate |
| order | .authorizations[0].uri | auth_uri | {auth_uri} | | order | .authorizations[0].uri | auth_uri | {auth_uri} |
| order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} |
+14 -14
View File
@@ -2,7 +2,7 @@ Feature: Order
Scenario: Create a new order Scenario: Create a new order
Given I have an ACME cert profile as "acme_profile" 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 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 When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -14,15 +14,15 @@ Feature: Order
Then I create a RSA private key pair as cert_key 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 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 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 ".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 ".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 ".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/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true 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 Scenario: Create a new order with SANs
Given I have an ACME cert profile as "acme_profile" 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 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 When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -52,7 +52,7 @@ Feature: Order
Scenario: Fetch an order Scenario: Fetch an order
Given I have an ACME cert profile as "acme_profile" 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 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 When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -67,21 +67,21 @@ Feature: Order
And I send an ACME post-as-get to order.uri as fetched_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 ".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 ".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 ".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/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true 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 Scenario Outline: Create an order with invalid identifier types
Given I have an ACME cert profile as "acme_profile" 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 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 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": { "protected": {
"alg": "RS256", "alg": "RS256",
"nonce": "{nonce}", "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}" "kid": "{acme_account.uri}"
}, },
"payload": { "payload": {
@@ -105,16 +105,16 @@ Feature: Order
Scenario Outline: Create an order with invalid identifier values Scenario Outline: Create an order with invalid identifier values
Given I have an ACME cert profile as "acme_profile" 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 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 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": { "protected": {
"alg": "RS256", "alg": "RS256",
"nonce": "{nonce}", "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}" "kid": "{acme_account.uri}"
}, },
"payload": { "payload": {
+198 -32
View File
@@ -2,6 +2,8 @@ import json
import logging import logging
import re import re
import urllib.parse import urllib.parse
import time
import threading
import acme.client import acme.client
import jq import jq
@@ -18,6 +20,10 @@ from josepy.jwk import JWKRSA
from josepy import json_util from josepy import json_util
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa 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 import x509
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
@@ -56,7 +62,7 @@ def step_impl(context: Context, profile_var: str):
profile_slug = faker.slug() profile_slug = faker.slug()
jwt_token = context.vars["AUTH_TOKEN"] jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post( response = context.http_client.post(
"/api/v1/pki/certificate-profiles", "/api/v1/cert-manager/certificate-profiles",
headers=dict(authorization="Bearer {}".format(jwt_token)), headers=dict(authorization="Bearer {}".format(jwt_token)),
json={ json={
"projectId": context.vars["PROJECT_ID"], "projectId": context.vars["PROJECT_ID"],
@@ -74,7 +80,7 @@ def step_impl(context: Context, profile_var: str):
kid = profile_id kid = profile_id
response = context.http_client.get( 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)), headers=dict(authorization="Bearer {}".format(jwt_token)),
) )
response.raise_for_status() response.raise_for_status()
@@ -147,13 +153,47 @@ def step_impl(context: Context, var_name: str):
context.vars[var_name] = response context.vars[var_name] = response
@given("I create a DNS Made Easy connection as {var_name}")
def step_impl(context: Context, var_name: str):
jwt_token = context.vars["AUTH_TOKEN"]
conn_slug = faker.slug()
with with_nocks(
context,
definitions=[
{
"scope": "https://api.dnsmadeeasy.com:443",
"method": "GET",
"path": "/V2.0/dns/managed/",
"status": 200,
"response": {"totalRecords": 0, "totalPages": 1, "data": [], "page": 0},
"responseIsBinary": False,
}
],
):
response = context.http_client.post(
"/api/v1/app-connections/dns-made-easy",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"name": conn_slug,
"description": "",
"method": "api-key-secret",
"credentials": {
"apiKey": "MOCK_API_KEY",
"secretKey": "MOCK_SECRET_KEY",
},
},
)
response.raise_for_status()
context.vars[var_name] = response
@given("I create a external ACME CA with the following config as {var_name}") @given("I create a external ACME CA with the following config as {var_name}")
def step_impl(context: Context, var_name: str): def step_impl(context: Context, var_name: str):
jwt_token = context.vars["AUTH_TOKEN"] jwt_token = context.vars["AUTH_TOKEN"]
ca_slug = faker.slug() ca_slug = faker.slug()
config = replace_vars(json.loads(context.text), context.vars) config = replace_vars(json.loads(context.text), context.vars)
response = context.http_client.post( response = context.http_client.post(
"/api/v1/pki/ca/acme", "/api/v1/cert-manager/ca/acme",
headers=dict(authorization="Bearer {}".format(jwt_token)), headers=dict(authorization="Bearer {}".format(jwt_token)),
json={ json={
"projectId": context.vars["PROJECT_ID"], "projectId": context.vars["PROJECT_ID"],
@@ -174,7 +214,7 @@ def step_impl(context: Context, var_name: str):
template_slug = faker.slug() template_slug = faker.slug()
config = replace_vars(json.loads(context.text), context.vars) config = replace_vars(json.loads(context.text), context.vars)
response = context.http_client.post( response = context.http_client.post(
"/api/v2/certificate-templates", "/api/v1/cert-manager/certificate-templates",
headers=dict(authorization="Bearer {}".format(jwt_token)), headers=dict(authorization="Bearer {}".format(jwt_token)),
json={ json={
"projectId": context.vars["PROJECT_ID"], "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() profile_slug = faker.slug()
jwt_token = context.vars["AUTH_TOKEN"] jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post( response = context.http_client.post(
"/api/v1/pki/certificate-profiles", "/api/v1/cert-manager/certificate-profiles",
headers=dict(authorization="Bearer {}".format(jwt_token)), headers=dict(authorization="Bearer {}".format(jwt_token)),
json={ json={
"projectId": context.vars["PROJECT_ID"], "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 kid = profile_id
response = context.http_client.get( 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)), headers=dict(authorization="Bearer {}".format(jwt_token)),
) )
response.raise_for_status() response.raise_for_status()
@@ -236,7 +276,7 @@ def step_impl(context: Context, profile_var: str):
profile_slug = faker.slug() profile_slug = faker.slug()
jwt_token = context.vars["AUTH_TOKEN"] jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post( response = context.http_client.post(
"/api/v1/pki/certificate-profiles", "/api/v1/cert-manager/certificate-profiles",
headers=dict(authorization="Bearer {}".format(jwt_token)), headers=dict(authorization="Bearer {}".format(jwt_token)),
json={ json={
"projectId": context.vars["PROJECT_ID"], "projectId": context.vars["PROJECT_ID"],
@@ -254,7 +294,7 @@ def step_impl(context: Context, profile_var: str):
kid = profile_id kid = profile_id
response = context.http_client.get( 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)), headers=dict(authorization="Bearer {}".format(jwt_token)),
) )
response.raise_for_status() 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 gen_private_key(key_type: str):
def step_impl(context: Context, rsa_key_var: str): if key_type == "RSA-2048" or key_type == "RSA":
context.vars[rsa_key_var] = rsa.generate_private_key( return rsa.generate_private_key(
# TODO: make them configurable if we need to public_exponent=65537,
public_exponent=65537, key_size=2048,
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" "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): def step_impl(context: Context, csr_var: str, pk_var: str, pem_var: str):
context.vars[pem_var] = ( context.vars[pem_var] = sign_csr(
context.vars[csr_var] pem=context.vars[csr_var],
.sign(context.vars[pk_var], hashes.SHA256()) pk=context.vars[pk_var],
.public_bytes(serialization.Encoding.PEM)
) )
@@ -690,6 +774,15 @@ def step_impl(context: Context, var_path: str, jq_query, var_name: str):
context.vars[var_name] = value 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}") @then("I peak and memorize the next nonce as {var_name}")
def step_impl(context: Context, var_name: str): def step_impl(context: Context, var_name: str):
acme_client = context.acme_client acme_client = context.acme_client
@@ -763,22 +856,39 @@ def select_challenge(
return challenges[0] return challenges[0]
def serve_challenge( def serve_challenges(
context: Context, context: Context,
challenge: messages.ChallengeBody, challenges: list[messages.ChallengeBody],
wait_time: int | None = None,
): ):
if hasattr(context, "web_server"): if hasattr(context, "web_server"):
context.web_server.shutdown_and_server_close() context.web_server.shutdown_and_server_close()
response, validation = challenge.response_and_validation( resources = set()
context.acme_client.net.key for challenge in challenges:
) response, validation = challenge.response_and_validation(
resource = standalone.HTTP01RequestHandler.HTTP01Resource( context.acme_client.net.key
chall=challenge.chall, response=response, validation=validation )
) resources.add(
standalone.HTTP01RequestHandler.HTTP01Resource(
chall=challenge.chall, response=response, validation=validation
)
)
# TODO: make port configurable # TODO: make port configurable
servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), {resource}) servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), resources)
servers.serve_forever() 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 context.web_server = servers
@@ -831,6 +941,7 @@ def step_impl(
f"Expected OrderResource but got {type(order)!r} at {order_var_path!r}" f"Expected OrderResource but got {type(order)!r} at {order_var_path!r}"
) )
challenges = {}
for domain in order.body.identifiers: for domain in order.body.identifiers:
logger.info( logger.info(
"Selecting challenge for domain %s with type %s ...", "Selecting challenge for domain %s with type %s ...",
@@ -855,18 +966,28 @@ def step_impl(
domain.value, domain.value,
challenge_type, 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( logger.info(
"Notifying challenge for domain %s with type %s ...", domain, challenge_type "Notifying challenge for domain %s with type %s ...", domain, challenge_type
) )
notify_challenge_ready(context=context, challenge=challenge) 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}") @then("I serve challenge response for {var_path} at {hostname}")
def step_impl(context: Context, var_path: str, hostname: str): def step_impl(context: Context, var_path: str, hostname: str):
challenge = eval_var(context, var_path, as_json=False) 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") @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) 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}") @then("I poll and finalize the ACME order {var_path} as {finalized_var}")
def step_impl(context: Context, var_path: str, finalized_var: str): def step_impl(context: Context, var_path: str, finalized_var: str):
order = eval_var(context, var_path, as_json=False) order = eval_var(context, var_path, as_json=False)
acme_client = context.acme_client acme_client = context.acme_client
finalized_order = acme_client.poll_and_finalize(order) try:
context.vars[finalized_var] = finalized_order 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}") @then("I parse the full-chain certificate from order {order_var_path} as {cert_var}")
+1
View File
@@ -25,6 +25,7 @@
"outputPath": "binary" "outputPath": "binary"
}, },
"scripts": { "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: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:package": "pkg --no-bytecode --public-packages \"*\" --public --target host .",
"binary:babel-backend": " babel ./dist -d ./dist", "binary:babel-backend": " babel ./dist -d ./dist",
+75
View File
@@ -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
+2
View File
@@ -65,6 +65,7 @@ import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-a
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { TCertificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service"; import { TCertificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
import { TCertificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-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 { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service"; import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
@@ -288,6 +289,7 @@ declare module "fastify" {
auditLogStream: TAuditLogStreamServiceFactory; auditLogStream: TAuditLogStreamServiceFactory;
certificate: TCertificateServiceFactory; certificate: TCertificateServiceFactory;
certificateV3: TCertificateV3ServiceFactory; certificateV3: TCertificateV3ServiceFactory;
certificateRequest: TCertificateRequestServiceFactory;
certificateTemplate: TCertificateTemplateServiceFactory; certificateTemplate: TCertificateTemplateServiceFactory;
certificateTemplateV2: TCertificateTemplateV2ServiceFactory; certificateTemplateV2: TCertificateTemplateV2ServiceFactory;
certificateProfile: TCertificateProfileServiceFactory; certificateProfile: TCertificateProfileServiceFactory;
+10
View File
@@ -573,6 +573,11 @@ import {
TWorkflowIntegrationsInsert, TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate TWorkflowIntegrationsUpdate
} from "@app/db/schemas"; } from "@app/db/schemas";
import {
TCertificateRequests,
TCertificateRequestsInsert,
TCertificateRequestsUpdate
} from "@app/db/schemas/certificate-requests";
import { import {
TAccessApprovalPoliciesEnvironments, TAccessApprovalPoliciesEnvironments,
TAccessApprovalPoliciesEnvironmentsInsert, TAccessApprovalPoliciesEnvironmentsInsert,
@@ -714,6 +719,11 @@ declare module "knex/types/tables" {
TExternalCertificateAuthoritiesUpdate TExternalCertificateAuthoritiesUpdate
>; >;
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>; [TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
[TableName.CertificateRequests]: KnexOriginal.CompositeTableType<
TCertificateRequests,
TCertificateRequestsInsert,
TCertificateRequestsUpdate
>;
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType< [TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
TCertificateTemplates, TCertificateTemplates,
TCertificateTemplatesInsert, TCertificateTemplatesInsert,
@@ -0,0 +1,27 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasIssuerTypeColumn = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "issuerType");
if (!hasIssuerTypeColumn) {
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
t.string("issuerType").notNullable().defaultTo("ca");
});
}
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
t.uuid("caId").nullable().alter();
});
}
export async function down(knex: Knex): Promise<void> {
const hasIssuerTypeColumn = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "issuerType");
if (hasIssuerTypeColumn) {
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
t.dropColumn("issuerType");
});
}
}
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
const hasCol = await knex.schema.hasColumn(TableName.ScimToken, "expiryNotificationSent");
if (hasCol) {
await knex.schema.alterTable(TableName.ScimToken, (t) => {
t.dropColumn("expiryNotificationSent");
});
}
}
@@ -0,0 +1,47 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
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<void> {
await knex.schema.dropTableIfExists(TableName.CertificateRequests);
await dropOnUpdateTrigger(knex, TableName.CertificateRequests);
}
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
const hasExternalConfigs = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "externalConfigs");
if (hasExternalConfigs) {
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
t.dropColumn("externalConfigs");
});
}
}
@@ -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<typeof CertificateRequestsSchema>;
export type TCertificateRequestsInsert = Omit<z.input<typeof CertificateRequestsSchema>, TImmutableDBKeys>;
export type TCertificateRequestsUpdate = Partial<Omit<z.input<typeof CertificateRequestsSchema>, TImmutableDBKeys>>;
+1
View File
@@ -16,6 +16,7 @@ export * from "./certificate-authority-certs";
export * from "./certificate-authority-crl"; export * from "./certificate-authority-crl";
export * from "./certificate-authority-secret"; export * from "./certificate-authority-secret";
export * from "./certificate-bodies"; export * from "./certificate-bodies";
export * from "./certificate-requests";
export * from "./certificate-secrets"; export * from "./certificate-secrets";
export * from "./certificate-syncs"; export * from "./certificate-syncs";
export * from "./certificate-template-est-configs"; export * from "./certificate-template-est-configs";
+1
View File
@@ -21,6 +21,7 @@ export enum TableName {
CertificateAuthorityCrl = "certificate_authority_crl", CertificateAuthorityCrl = "certificate_authority_crl",
Certificate = "certificates", Certificate = "certificates",
CertificateBody = "certificate_bodies", CertificateBody = "certificate_bodies",
CertificateRequests = "certificate_requests",
CertificateSecret = "certificate_secrets", CertificateSecret = "certificate_secrets",
CertificateTemplate = "certificate_templates", CertificateTemplate = "certificate_templates",
PkiCertificateTemplateV2 = "pki_certificate_templates_v2", PkiCertificateTemplateV2 = "pki_certificate_templates_v2",
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const PkiCertificateProfilesSchema = z.object({ export const PkiCertificateProfilesSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
projectId: z.string(), projectId: z.string(),
caId: z.string().uuid(), caId: z.string().uuid().nullable().optional(),
certificateTemplateId: z.string().uuid(), certificateTemplateId: z.string().uuid(),
slug: z.string(), slug: z.string(),
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
@@ -19,7 +19,9 @@ export const PkiCertificateProfilesSchema = z.object({
apiConfigId: z.string().uuid().nullable().optional(), apiConfigId: z.string().uuid().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
acmeConfigId: z.string().uuid().nullable().optional() acmeConfigId: z.string().uuid().nullable().optional(),
issuerType: z.string().default("ca"),
externalConfigs: z.string().nullable().optional()
}); });
export type TPkiCertificateProfiles = z.infer<typeof PkiCertificateProfilesSchema>; export type TPkiCertificateProfiles = z.infer<typeof PkiCertificateProfilesSchema>;
+2 -1
View File
@@ -13,7 +13,8 @@ export const ScimTokensSchema = z.object({
description: z.string(), description: z.string(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
expiryNotificationSent: z.boolean().default(false).nullable().optional()
}); });
export type TScimTokens = z.infer<typeof ScimTokensSchema>; export type TScimTokens = z.infer<typeof ScimTokensSchema>;
+1 -1
View File
@@ -110,7 +110,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" }); await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" }); await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" });
}, },
{ prefix: "/pki" } { prefix: "/cert-manager" }
); );
await server.register( await server.register(
+12 -11
View File
@@ -77,7 +77,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
done(error, undefined); done(error, undefined);
} }
}); });
// GET /api/v1/pki/acme/profiles/<profile_id>/directory
// GET /api/v1/cert-manager/acme/profiles/<profile_id>/directory
// Directory (RFC 8555 Section 7.1.1) // Directory (RFC 8555 Section 7.1.1)
server.route({ server.route({
method: "GET", method: "GET",
@@ -99,7 +100,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
handler: async (req) => server.services.pkiAcme.getAcmeDirectory(req.params.profileId) handler: async (req) => server.services.pkiAcme.getAcmeDirectory(req.params.profileId)
}); });
// HEAD /api/v1/pki/acme/profiles/<profile_id>/new-nonce // HEAD /api/v1/cert-manager/acme/profiles/<profile_id>/new-nonce
// New Nonce (RFC 8555 Section 7.2) // New Nonce (RFC 8555 Section 7.2)
server.route({ server.route({
method: "HEAD", method: "HEAD",
@@ -126,7 +127,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}); });
// POST /api/v1/pki/acme/profiles/<profile_id>/new-account // POST /api/v1/cert-manager/acme/profiles/<profile_id>/new-account
// New Account (RFC 8555 Section 7.3) // New Account (RFC 8555 Section 7.3)
server.route({ server.route({
method: "POST", method: "POST",
@@ -163,7 +164,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}); });
// POST /api/v1/pki/acme/profiles/<profile_id>/accounts/<account_id> // POST /api/v1/cert-manager/acme/profiles/<profile_id>/accounts/<account_id>
// Account Deactivation (RFC 8555 Section 7.3.6) // Account Deactivation (RFC 8555 Section 7.3.6)
server.route({ server.route({
method: "POST", method: "POST",
@@ -200,7 +201,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}); });
// POST /api/v1/pki/acme/profiles/<profile_id>/new-order // POST /api/v1/cert-manager/acme/profiles/<profile_id>/new-order
// New Certificate Order (RFC 8555 Section 7.4) // New Certificate Order (RFC 8555 Section 7.4)
server.route({ server.route({
method: "POST", method: "POST",
@@ -235,7 +236,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}); });
// POST /api/v1/pki/acme/profiles/<profile_id>/orders/<order_id> // POST /api/v1/cert-manager/acme/profiles/<profile_id>/orders/<order_id>
// Get Order (RFC 8555 Section 7.1.3) // Get Order (RFC 8555 Section 7.1.3)
server.route({ server.route({
method: "POST", method: "POST",
@@ -271,7 +272,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}); });
// POST /api/v1/pki/acme/profiles/<profile_id>/orders/<order_id>/finalize // POST /api/v1/cert-manager/acme/profiles/<profile_id>/orders/<order_id>/finalize
// Applying for Certificate Issuance (RFC 8555 Section 7.4) // Applying for Certificate Issuance (RFC 8555 Section 7.4)
server.route({ server.route({
method: "POST", method: "POST",
@@ -308,7 +309,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
); );
} }
}); });
// POST /api/v1/pki/acme/profiles/<profile_id>/accounts/<account_id>/orders // POST /api/v1/cert-manager/acme/profiles/<profile_id>/accounts/<account_id>/orders
// List Orders (RFC 8555 Section 7.1.2.1) // List Orders (RFC 8555 Section 7.1.2.1)
server.route({ server.route({
method: "POST", method: "POST",
@@ -344,7 +345,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}); });
// POST /api/v1/pki/acme/profiles/<profile_id>/orders/<order_id>/certificate // POST /api/v1/cert-manager/acme/profiles/<profile_id>/orders/<order_id>/certificate
// Download Certificate (RFC 8555 Section 7.4.2) // Download Certificate (RFC 8555 Section 7.4.2)
server.route({ server.route({
method: "POST", method: "POST",
@@ -377,7 +378,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}); });
// POST /api/v1/pki/acme/profiles/<profile_id>/authorizations/<authz_id> // POST /api/v1/cert-manager/acme/profiles/<profile_id>/authorizations/<authz_id>
// Identifier Authorization (RFC 8555 Section 7.5) // Identifier Authorization (RFC 8555 Section 7.5)
server.route({ server.route({
method: "POST", method: "POST",
@@ -411,7 +412,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}); });
// POST /api/v1/pki/acme/profiles/<profile_id>/authorizations/<authz_id>/challenges/<challenge_id> // POST /api/v1/cert-manager/acme/profiles/<profile_id>/authorizations/<authz_id>/challenges/<challenge_id>
// Respond to Challenge (RFC 8555 Section 7.5.1) // Respond to Challenge (RFC 8555 Section 7.5.1)
server.route({ server.route({
method: "POST", method: "POST",
@@ -72,7 +72,6 @@ const ProjectTemplateEnvironmentsSchema = z
position: z.number().min(1) position: z.number().min(1)
}) })
.array() .array()
.min(1)
.superRefine((environments, ctx) => { .superRefine((environments, ctx) => {
if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES) if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" }); 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), description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles), roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type), type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type),
environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional() environments: ProjectTemplateEnvironmentsSchema.nullish().describe(ProjectTemplates.CREATE.environments)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -243,7 +242,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
.describe(ProjectTemplates.UPDATE.name), .describe(ProjectTemplates.UPDATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description), description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),
roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles), roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles),
environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments) environments: ProjectTemplateEnvironmentsSchema.nullish().describe(ProjectTemplates.UPDATE.environments)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -158,6 +158,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}, },
data: { data: {
...req.body, ...req.body,
name: req.body.slug,
...req.body.type, ...req.body.type,
permissions: req.body.permissions || undefined permissions: req.body.permissions || undefined
} }
@@ -243,7 +243,7 @@ export const accessApprovalRequestServiceFactory = ({
); );
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; 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 approvalPath = `${projectPath}/approval`;
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
@@ -399,7 +399,7 @@ export const accessApprovalRequestServiceFactory = ({
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`; const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const editorFullName = `${editedByUser.firstName} ${editedByUser.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 approvalPath = `${projectPath}/approval`;
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
@@ -766,7 +766,7 @@ export const accessApprovalRequestServiceFactory = ({
.map((appUser) => appUser.email) .map((appUser) => appUser.email)
.filter((email): email is string => !!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}`; const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
await notificationService.createUserNotifications( await notificationService.createUserNotifications(
@@ -388,6 +388,9 @@ export enum EventType {
GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle", GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle",
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config", UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-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_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration", ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
@@ -2787,6 +2790,7 @@ interface CreateCertificateProfile {
name: string; name: string;
projectId: string; projectId: string;
enrollmentType: string; enrollmentType: string;
issuerType: string;
}; };
} }
@@ -2845,7 +2849,6 @@ interface OrderCertificateFromProfile {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE; type: EventType.ORDER_CERTIFICATE_FROM_PROFILE;
metadata: { metadata: {
certificateProfileId: string; certificateProfileId: string;
orderId: string;
profileName: 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 = export type Event =
| CreateSubOrganizationEvent | CreateSubOrganizationEvent
| UpdateSubOrganizationEvent | UpdateSubOrganizationEvent
@@ -4574,6 +4602,9 @@ export type Event =
| PamResourceDeleteEvent | PamResourceDeleteEvent
| UpdateCertificateRenewalConfigEvent | UpdateCertificateRenewalConfigEvent
| DisableCertificateRenewalConfigEvent | DisableCertificateRenewalConfigEvent
| CreateCertificateRequestEvent
| GetCertificateRequestEvent
| GetCertificateFromRequestEvent
| AutomatedRenewCertificate | AutomatedRenewCertificate
| AutomatedRenewCertificateFailed | AutomatedRenewCertificateFailed
| UserLoginEvent | UserLoginEvent
@@ -1,10 +1,18 @@
import { ProjectMembershipRole } from "@app/db/schemas";
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
import { getConfig } from "@app/lib/config/env";
import { applyJitter } from "@app/lib/delay";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
@@ -15,7 +23,12 @@ import { TDynamicSecretLeaseConfig } from "./dynamic-secret-lease-types";
type TDynamicSecretLeaseQueueServiceFactoryDep = { type TDynamicSecretLeaseQueueServiceFactoryDep = {
queueService: TQueueServiceFactory; queueService: TQueueServiceFactory;
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">; dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">; smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "findById">;
identityDAL: TIdentityDALFactory;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById" | "findOne">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
projectDAL: Pick<TProjectDALFactory, "findById">;
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>; dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findById">; folderDAL: Pick<TSecretFolderDALFactory, "findById">;
@@ -23,18 +36,24 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
export type TDynamicSecretLeaseQueueServiceFactory = { export type TDynamicSecretLeaseQueueServiceFactory = {
pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise<void>; pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise<void>;
setLeaseRevocation: (leaseId: string, expiryAt: Date) => Promise<void>; setLeaseRevocation: (leaseId: string, dynamicSecretId: string, expiryAt: Date) => Promise<void>;
unsetLeaseRevocation: (leaseId: string) => Promise<void>; unsetLeaseRevocation: (leaseId: string) => Promise<void>;
queueFailedRevocation: (leaseId: string, dynamicSecretId: string) => Promise<void>;
init: () => Promise<void>; init: () => Promise<void>;
}; };
const MAX_REVOCATION_RETRY_COUNT = 10;
export const dynamicSecretLeaseQueueServiceFactory = ({ export const dynamicSecretLeaseQueueServiceFactory = ({
queueService, queueService,
dynamicSecretDAL, dynamicSecretDAL,
dynamicSecretProviders, dynamicSecretProviders,
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
kmsService, kmsService,
folderDAL folderDAL,
projectMembershipDAL,
projectDAL,
smtpService
}: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => { }: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queuePg<QueueName.DynamicSecretRevocation>( await queueService.queuePg<QueueName.DynamicSecretRevocation>(
@@ -48,10 +67,10 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
); );
}; };
const setLeaseRevocation = async (leaseId: string, expiryAt: Date) => { const setLeaseRevocation = async (leaseId: string, dynamicSecretId: string, expiryAt: Date) => {
await queueService.queuePg<QueueName.DynamicSecretRevocation>( await queueService.queuePg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretRevocation, QueueJobs.DynamicSecretRevocation,
{ leaseId }, { leaseId, dynamicSecretId },
{ {
id: leaseId, id: leaseId,
singletonKey: leaseId, singletonKey: leaseId,
@@ -68,10 +87,53 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId); await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
}; };
const queueFailedRevocation = async (leaseId: string, dynamicSecretId: string) => {
const appConfig = getConfig();
const retryDelaySeconds = appConfig.isDevelopmentMode ? 1 : Math.floor(applyJitter(3_600_000 * 4) / 1000); // retry every 4 hours with 20% +- jitter (convert ms to seconds for pgboss)
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretRevocation,
{ leaseId, isRetry: true, dynamicSecretId },
{
singletonKey: `${leaseId}-retry`, // avoid conflicts with scheduled revocation
retryDelay: retryDelaySeconds,
retryLimit: MAX_REVOCATION_RETRY_COUNT, // we dont want it to ever hit the limit, we want the expireInHours to take effect.
expireInHours: 23 // if we set it to 24 hours, pgboss will complain that the expireIn is too high
}
);
};
const $queueDynamicSecretLeaseRevocationFailedEmail = async (leaseId: string, dynamicSecretId: string) => {
const appConfig = getConfig();
const delay = appConfig.isDevelopmentMode ? 1_000 * 60 : 1_000 * 60 * 15; // 1 minute in development, 15 minutes in production
await queueService.queue(
QueueName.DynamicSecretLeaseRevocationFailedEmail,
QueueJobs.DynamicSecretLeaseRevocationFailedEmail,
{
leaseId
},
{
jobId: `dynamic-secret-lease-revocation-failed-email-${dynamicSecretId}`,
delay,
attempts: 3,
backoff: {
type: "exponential",
delay: 1000 * 60 // 1 minute
},
removeOnComplete: true,
removeOnFail: true
}
);
};
const $dynamicSecretQueueJob = async ( const $dynamicSecretQueueJob = async (
jobName: string, jobName: string,
jobId: string, jobId: string,
data: { leaseId: string } | { dynamicSecretCfgId: string } data: { leaseId: string; dynamicSecretId: string; isRetry?: boolean } | { dynamicSecretCfgId: string },
retryCount?: number
): Promise<void> => { ): Promise<void> => {
try { try {
if (jobName === QueueJobs.DynamicSecretRevocation) { if (jobName === QueueJobs.DynamicSecretRevocation) {
@@ -79,7 +141,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
logger.info("Dynamic secret lease revocation started: ", leaseId, jobId); logger.info("Dynamic secret lease revocation started: ", leaseId, jobId);
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId); const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); if (!dynamicSecretLease) {
throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
}
const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId); const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId);
if (!folder) if (!folder)
@@ -150,7 +214,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
} }
logger.info("Finished dynamic secret job", jobId); logger.info("Finished dynamic secret job", jobId);
} catch (error) { } catch (error) {
logger.error(error); logger.error(error, "Failed to delete dynamic secret");
if (jobName === QueueJobs.DynamicSecretPruning) { if (jobName === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string }; const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
@@ -161,20 +225,97 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
} }
if (jobName === QueueJobs.DynamicSecretRevocation) { if (jobName === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = data as { leaseId: string }; const { leaseId, isRetry, dynamicSecretId } = data as {
leaseId: string;
isRetry?: boolean;
dynamicSecretId: string;
};
await dynamicSecretLeaseDAL.updateById(leaseId, { await dynamicSecretLeaseDAL.updateById(leaseId, {
status: DynamicSecretStatus.FailedDeletion, status: DynamicSecretStatus.FailedDeletion,
statusDetails: (error as Error)?.message?.slice(0, 255) statusDetails: `${(error as Error)?.message?.slice(0, 255)} - Retrying automatically`
}); });
// only add to retry queue if this is not a retry, and if the error is not a DisableRotationErrors error
if (!isRetry && !(error instanceof DisableRotationErrors)) {
// if revocation fails, we should stop the job and queue a new job to retry the revocation at a later time.
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
await queueFailedRevocation(leaseId, dynamicSecretId);
// if its the last attempt, and the error isn't a DisableRotationErrors error, send an email to the project admins (debounced)
} else if (isRetry && !(error instanceof DisableRotationErrors)) {
if (retryCount && retryCount === MAX_REVOCATION_RETRY_COUNT) {
// if all retries fail, we should also stop the automatic revocation job.
// the ID of the revocation job is set to the leaseId, so we can use that to stop the job
// we dont have to stop the retry job, because if we hit this point, its the last attempt and the retry job will be stopped by pgboss itself after this point,
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, leaseId);
await $queueDynamicSecretLeaseRevocationFailedEmail(leaseId, dynamicSecretId);
}
}
} }
if (error instanceof DisableRotationErrors) { if (error instanceof DisableRotationErrors) {
if (jobId) { if (jobId) {
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId); await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId); await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
} }
} else {
// 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); await $dynamicSecretQueueJob(job.name, job.id as string, job.data);
}); });
// we use redis for sending the email because:
// 1. we are insensitive to losing the jobs in queue in case of a disaster event
// 2. pgboss does not support exclusive job keys on v0.10.x, and upgrading to v0.11.x which supports exclusive jobs comes with a lot of breaking changes, and we would need to manually migrate our existing jobs to the new version
queueService.start(QueueName.DynamicSecretLeaseRevocationFailedEmail, async (job) => {
await $dynamicSecretLeaseRevocationFailedEmailJob(job.id as string, job.data);
});
const init = async () => { const init = async () => {
await queueService.startPg<QueueName.DynamicSecretRevocation>( await queueService.startPg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretRevocation, QueueJobs.DynamicSecretRevocation,
async ([job]) => { async ([job]) => {
await $dynamicSecretQueueJob(job.name, job.id, job.data); await $dynamicSecretQueueJob(job.name, job.id, job.data, job.retryCount);
}, },
{ {
workerCount: 5, workerCount: 10,
pollingIntervalSeconds: 1 pollingIntervalSeconds: 1
} }
); );
@@ -210,6 +358,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
pruneDynamicSecret, pruneDynamicSecret,
setLeaseRevocation, setLeaseRevocation,
unsetLeaseRevocation, unsetLeaseRevocation,
queueFailedRevocation,
init init
}; };
}; };
@@ -178,7 +178,7 @@ export const dynamicSecretLeaseServiceFactory = ({
config config
}); });
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt); await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, dynamicSecretCfg.id, expireAt);
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data }; return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
}; };
@@ -272,7 +272,7 @@ export const dynamicSecretLeaseServiceFactory = ({
); );
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt); await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, dynamicSecretCfg.id, expireAt);
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
expireAt, expireAt,
externalEntityId: entityId externalEntityId: entityId
@@ -358,11 +358,13 @@ export const dynamicSecretLeaseServiceFactory = ({
if ((revokeResponse as { error?: Error })?.error) { if ((revokeResponse as { error?: Error })?.error) {
const { error } = revokeResponse as { error?: Error }; const { error } = revokeResponse as { error?: Error };
logger.error(error?.message, "Failed to revoke lease"); logger.error(error?.message, "Failed to revoke lease");
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, { const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
status: DynamicSecretLeaseStatus.FailedDeletion, status: DynamicSecretLeaseStatus.FailedDeletion,
statusDetails: error?.message?.slice(0, 255) statusDetails: error?.message?.slice(0, 255)
}); });
return deletedDynamicSecretLease; // queue a job to retry the revocation at a later time
await dynamicSecretQueueService.queueFailedRevocation(dynamicSecretLease.id, dynamicSecretCfg.id);
return updatedDynamicSecretLease;
} }
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id); await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
@@ -450,8 +450,8 @@ export const licenseServiceFactory = ({
} = await licenseServerCloudApi.request.post( } = await licenseServerCloudApi.request.post(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`, `/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`,
{ {
success_url: `${envConfig.SITE_URL}/organization/billing`, success_url: `${envConfig.SITE_URL}/organizations/${orgId}/billing`,
cancel_url: `${envConfig.SITE_URL}/organization/billing` cancel_url: `${envConfig.SITE_URL}/organizations/${orgId}/billing`
} }
); );
@@ -464,7 +464,7 @@ export const licenseServiceFactory = ({
} = await licenseServerCloudApi.request.post( } = await licenseServerCloudApi.request.post(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`, `/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`
} }
); );
@@ -44,7 +44,6 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.Settings, ProjectPermissionSub.Settings,
ProjectPermissionSub.Environments, ProjectPermissionSub.Environments,
ProjectPermissionSub.Tags, ProjectPermissionSub.Tags,
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList, ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities, ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.PkiAlerts, ProjectPermissionSub.PkiAlerts,
@@ -67,6 +66,8 @@ const buildAdminPermissionRules = () => {
); );
}); });
can([ProjectPermissionAuditLogsActions.Read], ProjectPermissionSub.AuditLogs);
can( can(
[ [
ProjectPermissionPkiTemplateActions.Read, ProjectPermissionPkiTemplateActions.Read,
@@ -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 { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isPrivateIp } from "@app/lib/ip/ipRange"; import { isPrivateIp } from "@app/lib/ip/ipRange";
@@ -13,14 +16,14 @@ import {
import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas"; import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas";
import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types"; import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types";
type FetchError = Error & {
code?: string;
};
type TPkiAcmeChallengeServiceFactoryDep = { type TPkiAcmeChallengeServiceFactoryDep = {
acmeChallengeDAL: Pick< acmeChallengeDAL: Pick<
TPkiAcmeChallengeDALFactory, TPkiAcmeChallengeDALFactory,
"transaction" | "findByIdForChallengeValidation" | "markAsValidCascadeById" | "markAsInvalidCascadeById" | "transaction"
| "findByIdForChallengeValidation"
| "markAsValidCascadeById"
| "markAsInvalidCascadeById"
| "updateById"
>; >;
}; };
@@ -28,9 +31,8 @@ export const pkiAcmeChallengeServiceFactory = ({
acmeChallengeDAL acmeChallengeDAL
}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => { }: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => {
const appCfg = getConfig(); const appCfg = getConfig();
const markChallengeAsReady = async (challengeId: string): Promise<TPkiAcmeChallenges> => {
const validateChallengeResponse = async (challengeId: string): Promise<void> => { return acmeChallengeDAL.transaction(async (tx) => {
const error: Error | undefined = await acmeChallengeDAL.transaction(async (tx) => {
logger.info({ challengeId }, "Validating ACME challenge response"); logger.info({ challengeId }, "Validating ACME challenge response");
const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx); const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx);
if (!challenge) { if (!challenge) {
@@ -54,89 +56,102 @@ export const pkiAcmeChallengeServiceFactory = ({
if (challenge.type !== AcmeChallengeType.HTTP_01) { if (challenge.type !== AcmeChallengeType.HTTP_01) {
throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" }); 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 // check if host is a private ip address
if (isPrivateIp(host)) { if (isPrivateIp(host)) {
throw new BadRequestError({ message: "Private IP addresses are not allowed" }); throw new BadRequestError({ message: "Private IP addresses are not allowed" });
} }
if (appCfg.isAcmeDevelopmentMode && appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]) { return acmeChallengeDAL.updateById(challengeId, { status: AcmeChallengeStatus.Processing }, tx);
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;
}
}); });
if (error) { };
throw error;
const validateChallengeResponse = async (challengeId: string, retryCount: number): Promise<void> => {
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<string>(challengeUrl.toString(), {
// In case if we override the host in the development mode, still provide the original host in the header
// to help the upstream server to validate the request
headers: { Host: 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 };
}; };
@@ -8,7 +8,7 @@ import { AcmeAccountDoesNotExistError } from "./pki-acme-errors";
export const buildUrl = (profileId: string, path: string): string => { export const buildUrl = (profileId: string, path: string): string => {
const appCfg = getConfig(); const appCfg = getConfig();
const baseUrl = appCfg.SITE_URL ?? ""; 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 => { export const extractAccountIdFromKid = (kid: string, profileId: string): string => {
@@ -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<ReturnType<typeof pkiAcmeQueueServiceFactory>>;
export const pkiAcmeQueueServiceFactory = async ({
queueService,
acmeChallengeService
}: TPkiAcmeQueueServiceFactoryDep) => {
const appCfg = getConfig();
// Initialize the worker to process challenge validation jobs
await queueService.startPg<QueueName.PkiAcmeChallengeValidation>(
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<void> => {
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
};
};
@@ -31,12 +31,17 @@ import { orderCertificate } from "@app/services/certificate-authority/acme/acme-
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal"; 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 { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { import {
EnrollmentType, EnrollmentType,
TCertificateProfileWithConfigs TCertificateProfileWithConfigs
} from "@app/services/certificate-profile/certificate-profile-types"; } 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 { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -62,6 +67,7 @@ import {
import { buildUrl, extractAccountIdFromKid, validateDnsIdentifier } from "./pki-acme-fns"; import { buildUrl, extractAccountIdFromKid, validateDnsIdentifier } from "./pki-acme-fns";
import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal";
import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal";
import { TPkiAcmeQueueServiceFactory } from "./pki-acme-queue";
import { import {
AcmeAuthStatus, AcmeAuthStatus,
AcmeChallengeStatus, AcmeChallengeStatus,
@@ -94,12 +100,13 @@ import {
type TPkiAcmeServiceFactoryDep = { type TPkiAcmeServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">; appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">; certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "updateById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">; externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">; certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">; certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateTemplateV2DAL: Pick<TCertificateTemplateV2DALFactory, "findById">;
acmeAccountDAL: Pick< acmeAccountDAL: Pick<
TPkiAcmeAccountDALFactory, TPkiAcmeAccountDALFactory,
"findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create"
@@ -126,7 +133,9 @@ type TPkiAcmeServiceFactoryDep = {
>; >;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">; certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
acmeChallengeService: TPkiAcmeChallengeServiceFactory; certificateTemplateV2Service: Pick<TCertificateTemplateV2ServiceFactory, "validateCertificateRequest">;
acmeChallengeService: Pick<TPkiAcmeChallengeServiceFactory, "markChallengeAsReady">;
pkiAcmeQueueService: Pick<TPkiAcmeQueueServiceFactory, "queueChallengeValidation">;
}; };
export const pkiAcmeServiceFactory = ({ export const pkiAcmeServiceFactory = ({
@@ -138,6 +147,7 @@ export const pkiAcmeServiceFactory = ({
certificateProfileDAL, certificateProfileDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL, certificateSecretDAL,
certificateTemplateV2DAL,
acmeAccountDAL, acmeAccountDAL,
acmeOrderDAL, acmeOrderDAL,
acmeAuthDAL, acmeAuthDAL,
@@ -147,7 +157,9 @@ export const pkiAcmeServiceFactory = ({
kmsService, kmsService,
licenseService, licenseService,
certificateV3Service, certificateV3Service,
acmeChallengeService certificateTemplateV2Service,
acmeChallengeService,
pkiAcmeQueueService
}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => { const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => {
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId); const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
@@ -683,6 +695,13 @@ export const pkiAcmeServiceFactory = ({
payload: TFinalizeAcmeOrderPayload; payload: TFinalizeAcmeOrderPayload;
}): Promise<TAcmeResponse<TAcmeOrderResource>> => { }): Promise<TAcmeResponse<TAcmeOrderResource>> => {
const profile = (await certificateProfileDAL.findByIdWithConfigs(profileId))!; const profile = (await certificateProfileDAL.findByIdWithConfigs(profileId))!;
if (!profile.caId) {
throw new BadRequestError({
message: "Self-signed certificates are not supported for ACME enrollment"
});
}
let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId);
if (!order) { if (!order) {
throw new NotFoundError({ message: "ACME order not found" }); throw new NotFoundError({ message: "ACME order not found" });
@@ -703,9 +722,6 @@ export const pkiAcmeServiceFactory = ({
// Check and validate the CSR // Check and validate the CSR
const certificateRequest = extractCertificateRequestFromCSR(csr); const certificateRequest = extractCertificateRequestFromCSR(csr);
if (!certificateRequest.commonName) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name is required" });
}
if ( if (
certificateRequest.subjectAlternativeNames?.some( certificateRequest.subjectAlternativeNames?.some(
(san) => san.type !== CertSubjectAlternativeNameType.DNS_NAME (san) => san.type !== CertSubjectAlternativeNameType.DNS_NAME
@@ -721,7 +737,7 @@ export const pkiAcmeServiceFactory = ({
const csrIdentifierValues = new Set( const csrIdentifierValues = new Set(
(certificateRequest.subjectAlternativeNames ?? []) (certificateRequest.subjectAlternativeNames ?? [])
.map((san) => san.value.toLowerCase()) .map((san) => san.value.toLowerCase())
.concat([certificateRequest.commonName.toLowerCase()]) .concat(certificateRequest.commonName ? [certificateRequest.commonName.toLowerCase()] : [])
); );
if ( if (
csrIdentifierValues.size !== orderWithAuthorizations.authorizations.length || 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" }); throw new AcmeBadCSRError({ message: "Invalid CSR: Common name + SANs mismatch with order identifiers" });
} }
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId!);
if (!ca) { if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" }); throw new NotFoundError({ message: "Certificate Authority not found" });
} }
@@ -765,14 +781,39 @@ export const pkiAcmeServiceFactory = ({
const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!; const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!;
const csrObj = new x509.Pkcs10CertificateRequest(csr); const csrObj = new x509.Pkcs10CertificateRequest(csr);
const csrPem = csrObj.toString("pem"); 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, // 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 // we should queue the certificate issuance to a background job instead
const cert = await orderCertificate( const cert = await orderCertificate(
{ {
caId: certificateAuthority!.id, caId: certificateAuthority!.id,
commonName: certificateRequest.commonName!, // It is possible that the CSR does not have a common name, in which case we use an empty string
// (more likely than not for a CSR from a modern ACME client like certbot, cert-manager, etc.)
commonName: certificateRequest.commonName ?? "",
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value), altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
csr: Buffer.from(csrPem), csr: Buffer.from(csrPem),
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now // TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
@@ -815,6 +856,8 @@ export const pkiAcmeServiceFactory = ({
// TODO: audit log the error // TODO: audit log the error
if (exp instanceof BadRequestError) { if (exp instanceof BadRequestError) {
errorToReturn = new AcmeBadCSRError({ message: `Invalid CSR: ${exp.message}` }); errorToReturn = new AcmeBadCSRError({ message: `Invalid CSR: ${exp.message}` });
} else if (exp instanceof AcmeError) {
errorToReturn = exp;
} else { } else {
errorToReturn = new AcmeServerInternalError({ message: "Failed to sign certificate with internal error" }); errorToReturn = new AcmeServerInternalError({ message: "Failed to sign certificate with internal error" });
} }
@@ -969,7 +1012,8 @@ export const pkiAcmeServiceFactory = ({
if (!result) { if (!result) {
throw new NotFoundError({ message: "ACME challenge not found" }); 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))!; const challenge = (await acmeChallengeDAL.findByIdForChallengeValidation(challengeId))!;
return { return {
status: 200, status: 200,
@@ -1,6 +1,8 @@
import { JWSHeaderParameters } from "jose"; import { JWSHeaderParameters } from "jose";
import { z } from "zod"; import { z } from "zod";
import { TPkiAcmeChallenges } from "@app/db/schemas/pki-acme-challenges";
import { import {
AcmeOrderResourceSchema, AcmeOrderResourceSchema,
CreateAcmeAccountBodySchema, CreateAcmeAccountBodySchema,
@@ -176,5 +178,6 @@ export type TPkiAcmeServiceFactory = {
}; };
export type TPkiAcmeChallengeServiceFactory = { export type TPkiAcmeChallengeServiceFactory = {
validateChallengeResponse: (challengeId: string) => Promise<void>; markChallengeAsReady: (challengeId: string) => Promise<TPkiAcmeChallenges>;
validateChallengeResponse: (challengeId: string, retryCount: number) => Promise<void>;
}; };
@@ -189,11 +189,15 @@ export const projectTemplateServiceFactory = ({
message: `A project template with the name "${params.name}" already exists.` 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({ const projectTemplate = await projectTemplateDAL.create({
...params, ...params,
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))), roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
environments: environments: JSON.stringify(projectTemplateEnvironments),
type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null,
orgId: actor.orgId, orgId: actor.orgId,
type type
}); });
@@ -622,7 +622,7 @@ export const samlConfigServiceFactory = ({
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL); const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
newUser = await userDAL.create( newUser = await userDAL.create(
{ {
username: serverCfg.trustSamlEmails ? email : uniqueUsername, username: serverCfg.trustSamlEmails ? email.toLowerCase() : uniqueUsername,
email, email,
isEmailVerified: serverCfg.trustSamlEmails, isEmailVerified: serverCfg.trustSamlEmails,
firstName, firstName,
@@ -639,7 +639,7 @@ export const samlConfigServiceFactory = ({
userId: newUser.id, userId: newUser.id,
aliasType: UserAliasType.SAML, aliasType: UserAliasType.SAML,
externalId, externalId,
emails: email ? [email] : [], emails: email ? [email.toLowerCase()] : [],
orgId, orgId,
isEmailVerified: serverCfg.trustSamlEmails isEmailVerified: serverCfg.trustSamlEmails
}, },
+51 -5
View File
@@ -1,10 +1,56 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
import { ormify, TOrmify } from "@app/lib/knex"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TScimDALFactory = TOrmify<TableName.ScimToken>; import { TExpiringScimToken } from "./scim-types";
export const scimDALFactory = (db: TDbClient): TScimDALFactory => { export type TScimDALFactory = ReturnType<typeof scimDALFactory>;
export const scimDALFactory = (db: TDbClient) => {
const scimTokenOrm = ormify(db, TableName.ScimToken); const scimTokenOrm = ormify(db, TableName.ScimToken);
return scimTokenOrm;
const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0): Promise<TExpiringScimToken[]> => {
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<TExpiringScimToken[]>([
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 };
}; };
+77 -12
View File
@@ -19,6 +19,7 @@ import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal"; import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal";
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
@@ -47,7 +48,7 @@ import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, p
import { TScimGroup, TScimServiceFactory } from "./scim-types"; import { TScimGroup, TScimServiceFactory } from "./scim-types";
type TScimServiceFactoryDep = { type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">; scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById" | "findExpiringTokens" | "update">;
userDAL: Pick< userDAL: Pick<
TUserDALFactory, TUserDALFactory,
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById" "find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
@@ -389,15 +390,13 @@ export const scimServiceFactory = ({
); );
} }
} else { } else {
if (trustScimEmails) { user = await userDAL.findOne(
user = await userDAL.findOne( {
{ email: email.toLowerCase(),
email: email.toLowerCase(), isEmailVerified: true
isEmailVerified: true },
}, tx
tx );
);
}
if (!user) { if (!user) {
const uniqueUsername = await normalizeUsername( const uniqueUsername = await normalizeUsername(
@@ -425,7 +424,8 @@ export const scimServiceFactory = ({
aliasType, aliasType,
externalId, externalId,
emails: email ? [email.toLowerCase()] : [], emails: email ? [email.toLowerCase()] : [],
orgId orgId,
isEmailVerified: trustScimEmails
}, },
tx tx
); );
@@ -1237,6 +1237,70 @@ export const scimServiceFactory = ({
return { scimTokenId: scimToken.id, orgId: scimToken.orgId }; 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 { return {
createScimToken, createScimToken,
listScimTokens, listScimTokens,
@@ -1253,6 +1317,7 @@ export const scimServiceFactory = ({
deleteScimGroup, deleteScimGroup,
replaceScimGroup, replaceScimGroup,
updateScimGroup, updateScimGroup,
fnValidateScimToken fnValidateScimToken,
notifyExpiringTokens
}; };
}; };
@@ -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 = { export type TScimServiceFactory = {
createScimToken: (arg: TCreateScimTokenDTO) => Promise<{ createScimToken: (arg: TCreateScimTokenDTO) => Promise<{
scimToken: string; scimToken: string;
@@ -200,4 +210,5 @@ export type TScimServiceFactory = {
scimTokenId: string; scimTokenId: string;
orgId: string; orgId: string;
}>; }>;
notifyExpiringTokens: () => Promise<number>;
}; };
@@ -37,7 +37,7 @@ export const sendApprovalEmailsFn = async ({
type: NotificationType.SECRET_CHANGE_REQUEST, type: NotificationType.SECRET_CHANGE_REQUEST,
title: "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}**.`, 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, firstName: reviewerUser.firstName,
projectName: project.name, projectName: project.name,
organizationName: project.organization.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 template: SmtpTemplates.SecretApprovalRequestNeedsReview
}); });
@@ -1037,7 +1037,7 @@ export const secretApprovalRequestServiceFactory = ({
bypassReason, bypassReason,
secretPath: policy.secretPath, secretPath: policy.secretPath,
environment: env.name, 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 template: SmtpTemplates.AccessSecretRequestBypassed
}); });
@@ -1416,7 +1416,7 @@ export const secretApprovalRequestServiceFactory = ({
const env = await projectEnvDAL.findOne({ id: policy.envId }); const env = await projectEnvDAL.findOne({ id: policy.envId });
const user = await userDAL.findById(actorId); 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 approvalPath = `${projectPath}/approval`;
const cfg = getConfig(); const cfg = getConfig();
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
@@ -1792,7 +1792,7 @@ export const secretApprovalRequestServiceFactory = ({
const user = await userDAL.findById(actorId); const user = await userDAL.findById(actorId);
const env = await projectEnvDAL.findOne({ id: policy.envId }); 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 approvalPath = `${projectPath}/approval`;
const cfg = getConfig(); const cfg = getConfig();
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
@@ -156,7 +156,7 @@ export const secretRotationV2QueueServiceFactory = async ({
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation]; 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( await notificationService.createUserNotifications(
projectAdmins.map((admin) => ({ projectAdmins.map((admin) => ({
@@ -637,7 +637,7 @@ export const secretScanningV2QueueServiceFactory = async ({
numberOfSecrets: payload.numberOfSecrets, numberOfSecrets: payload.numberOfSecrets,
isDiffScan: payload.isDiffScan, isDiffScan: payload.isDiffScan,
url: encodeURI( 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 timestamp
} }
@@ -648,7 +648,7 @@ export const secretScanningV2QueueServiceFactory = async ({
timestamp, timestamp,
errorMessage: payload.errorMessage, errorMessage: payload.errorMessage,
url: encodeURI( 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}`
) )
} }
}); });
+4
View File
@@ -1962,9 +1962,11 @@ export const CERTIFICATE_AUTHORITIES = {
export const CERTIFICATES = { export const CERTIFICATES = {
GET: { GET: {
id: "The ID of the certificate to get.",
serialNumber: "The serial number of the certificate to get." serialNumber: "The serial number of the certificate to get."
}, },
REVOKE: { REVOKE: {
id: "The ID of the certificate to revoke.",
serialNumber: serialNumber:
"The serial number of the certificate to revoke. The revoked certificate will be added to the certificate revocation list (CRL) of the CA.", "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.", revocationReason: "The reason for revoking the certificate.",
@@ -1972,9 +1974,11 @@ export const CERTIFICATES = {
serialNumberRes: "The serial number of the revoked certificate." serialNumberRes: "The serial number of the revoked certificate."
}, },
DELETE: { DELETE: {
id: "The ID of the certificate to delete.",
serialNumber: "The serial number of the certificate to delete." serialNumber: "The serial number of the certificate to delete."
}, },
GET_CERT: { 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.", serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
certificate: "The certificate body of the certificate.", certificate: "The certificate body of the certificate.",
certificateChain: "The certificate chain of the certificate.", certificateChain: "The certificate chain of the certificate.",
+1
View File
@@ -119,6 +119,7 @@ const envSchema = z
}) })
.default("{}") .default("{}")
), ),
DNS_MADE_EASY_SANDBOX_ENABLED: zodStrBool.default("false").optional(),
// smtp options // smtp options
SMTP_HOST: zpStr(z.string().optional()), SMTP_HOST: zpStr(z.string().optional()),
SMTP_IGNORE_TLS: zodStrBool.default("false"), SMTP_IGNORE_TLS: zodStrBool.default("false"),
+10
View File
@@ -2,3 +2,13 @@ export const delay = (ms: number) =>
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
setTimeout(resolve, ms); setTimeout(resolve, ms);
}); });
export const applyJitter = (delayMs: number) => {
const jitterFactor = 0.2;
// generates random value in [-0.2, +0.2] range
const randomFactor = (Math.random() * 2 - 1) * jitterFactor;
const jitterAmount = randomFactor * delayMs;
return delayMs + jitterAmount;
};
+35 -2
View File
@@ -61,8 +61,10 @@ export enum QueueName {
SecretPushEventScan = "secret-push-event-scan", SecretPushEventScan = "secret-push-event-scan",
UpgradeProjectToGhost = "upgrade-project-to-ghost", UpgradeProjectToGhost = "upgrade-project-to-ghost",
DynamicSecretRevocation = "dynamic-secret-revocation", DynamicSecretRevocation = "dynamic-secret-revocation",
DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email",
CaCrlRotation = "ca-crl-rotation", CaCrlRotation = "ca-crl-rotation",
CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber
CertificateIssuance = "certificate-issuance",
SecretReplication = "secret-replication", SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
PkiSync = "pki-sync", PkiSync = "pki-sync",
@@ -80,7 +82,8 @@ export enum QueueName {
UserNotification = "user-notification", UserNotification = "user-notification",
HealthAlert = "health-alert", HealthAlert = "health-alert",
CertificateV3AutoRenewal = "certificate-v3-auto-renewal", CertificateV3AutoRenewal = "certificate-v3-auto-renewal",
PamAccountRotation = "pam-account-rotation" PamAccountRotation = "pam-account-rotation",
PkiAcmeChallengeValidation = "pki-acme-challenge-validation"
} }
export enum QueueJobs { export enum QueueJobs {
@@ -120,11 +123,13 @@ export enum QueueJobs {
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification", SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
CreateFolderTreeCheckpoint = "create-folder-tree-checkpoint", CreateFolderTreeCheckpoint = "create-folder-tree-checkpoint",
DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email",
InvalidateCache = "invalidate-cache", InvalidateCache = "invalidate-cache",
SecretScanningV2FullScan = "secret-scanning-v2-full-scan", SecretScanningV2FullScan = "secret-scanning-v2-full-scan",
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan", SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan",
SecretScanningV2SendNotification = "secret-scanning-v2-notification", SecretScanningV2SendNotification = "secret-scanning-v2-notification",
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber", CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber",
CaIssueCertificateFromProfile = "ca-issue-certificate-from-profile",
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal", PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal",
TelemetryAggregatedEvents = "telemetry-aggregated-events", TelemetryAggregatedEvents = "telemetry-aggregated-events",
DailyReminders = "daily-reminders", DailyReminders = "daily-reminders",
@@ -132,7 +137,8 @@ export enum QueueJobs {
UserNotification = "user-notification-job", UserNotification = "user-notification-job",
HealthAlert = "health-alert", HealthAlert = "health-alert",
CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal", CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal",
PamAccountRotation = "pam-account-rotation" PamAccountRotation = "pam-account-rotation",
PkiAcmeChallengeValidation = "pki-acme-challenge-validation"
} }
export type TQueueJobTypes = { export type TQueueJobTypes = {
@@ -219,11 +225,19 @@ export type TQueueJobTypes = {
name: QueueJobs.TelemetryInstanceStats; name: QueueJobs.TelemetryInstanceStats;
payload: undefined; payload: undefined;
}; };
[QueueName.DynamicSecretLeaseRevocationFailedEmail]: {
name: QueueJobs.DynamicSecretLeaseRevocationFailedEmail;
payload: {
leaseId: string;
};
};
[QueueName.DynamicSecretRevocation]: [QueueName.DynamicSecretRevocation]:
| { | {
name: QueueJobs.DynamicSecretRevocation; name: QueueJobs.DynamicSecretRevocation;
payload: { payload: {
isRetry?: boolean;
leaseId: string; leaseId: string;
dynamicSecretId: string;
}; };
} }
| { | {
@@ -343,6 +357,21 @@ export type TQueueJobTypes = {
caType: CaType; 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]: { [QueueName.DailyReminders]: {
name: QueueJobs.DailyReminders; name: QueueJobs.DailyReminders;
payload: undefined; payload: undefined;
@@ -375,6 +404,10 @@ export type TQueueJobTypes = {
name: QueueJobs.PamAccountRotation; name: QueueJobs.PamAccountRotation;
payload: undefined; payload: undefined;
}; };
[QueueName.PkiAcmeChallengeValidation]: {
name: QueueJobs.PkiAcmeChallengeValidation;
payload: { challengeId: string };
};
}; };
const SECRET_SCANNING_JOBS = [ const SECRET_SCANNING_JOBS = [
@@ -6,7 +6,7 @@ import { DefaultResponseErrorsSchema } from "../routes/sanitizedSchemas";
const isScimRoutes = (pathname: string) => const isScimRoutes = (pathname: string) =>
pathname.startsWith("/api/v1/scim/Users") || pathname.startsWith("/api/v1/scim/Groups"); 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) => { export const addErrorsToResponseSchemas = fp(async (server) => {
server.addHook("onRoute", (routeOptions) => { server.addHook("onRoute", (routeOptions) => {
+9 -6
View File
@@ -43,7 +43,9 @@ export const registerServeUI = async (
const frontendPath = path.join(dir, frontendName); const frontendPath = path.join(dir, frontendName);
await server.register(staticServe, { await server.register(staticServe, {
root: frontendPath, root: frontendPath,
wildcard: false wildcard: false,
maxAge: "30d",
immutable: true
}); });
server.route({ server.route({
@@ -58,11 +60,12 @@ export const registerServeUI = async (
return; return;
} }
// This should help avoid caching any chunks (temp fix) return reply.sendFile("index.html", {
void reply.header("Cache-Control", "no-cache, no-store, must-revalidate, private, max-age=0"); immutable: false,
void reply.header("Pragma", "no-cache"); maxAge: 0,
void reply.header("Expires", "0"); lastModified: false,
return reply.sendFile("index.html"); etag: false
});
} }
}); });
} }
+58 -5
View File
@@ -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 { 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 { 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 { 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 { pkiAcmeServiceFactory } from "@app/ee/services/pki-acme/pki-acme-service";
import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal";
import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; 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 { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; 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 { externalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal";
import { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal/internal-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"; 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 { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service"; 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 { certificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-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 certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db);
const certificateSecretDAL = certificateSecretDALFactory(db); const certificateSecretDAL = certificateSecretDALFactory(db);
const certificateRequestDAL = certificateRequestDALFactory(db);
const certificateSyncDAL = certificateSyncDALFactory(db); const certificateSyncDAL = certificateSyncDALFactory(db);
const pkiAlertDAL = pkiAlertDALFactory(db); const pkiAlertDAL = pkiAlertDALFactory(db);
@@ -1187,7 +1192,7 @@ export const registerRoutes = async (
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL, certificateSecretDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, externalCertificateAuthorityDAL,
permissionService, permissionService,
licenseService, licenseService,
kmsService, kmsService,
@@ -1329,7 +1334,8 @@ export const registerRoutes = async (
eventBusService, eventBusService,
licenseService, licenseService,
membershipRoleDAL, membershipRoleDAL,
membershipUserDAL membershipUserDAL,
telemetryService
}); });
const projectService = projectServiceFactory({ const projectService = projectServiceFactory({
@@ -1874,7 +1880,12 @@ export const registerRoutes = async (
dynamicSecretProviders, dynamicSecretProviders,
dynamicSecretDAL, dynamicSecretDAL,
folderDAL, folderDAL,
kmsService kmsService,
smtpService,
userDAL,
identityDAL,
projectMembershipDAL,
projectDAL
}); });
const dynamicSecretService = dynamicSecretServiceFactory({ const dynamicSecretService = dynamicSecretServiceFactory({
projectDAL, projectDAL,
@@ -1907,6 +1918,7 @@ export const registerRoutes = async (
// DAILY // DAILY
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
scimService,
auditLogDAL, auditLogDAL,
queueService, queueService,
secretVersionDAL, secretVersionDAL,
@@ -2208,6 +2220,31 @@ export const registerRoutes = async (
pkiSyncQueue 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({ const certificateV3Service = certificateV3ServiceFactory({
certificateDAL, certificateDAL,
certificateSecretDAL, certificateSecretDAL,
@@ -2219,7 +2256,12 @@ export const registerRoutes = async (
permissionService, permissionService,
certificateSyncDAL, certificateSyncDAL,
pkiSyncDAL, pkiSyncDAL,
pkiSyncQueue pkiSyncQueue,
kmsService,
projectDAL,
certificateBodyDAL,
certificateIssuanceQueue,
certificateRequestService
}); });
const certificateV3Queue = certificateV3QueueServiceFactory({ const certificateV3Queue = certificateV3QueueServiceFactory({
@@ -2244,6 +2286,12 @@ export const registerRoutes = async (
const acmeChallengeService = pkiAcmeChallengeServiceFactory({ const acmeChallengeService = pkiAcmeChallengeServiceFactory({
acmeChallengeDAL acmeChallengeDAL
}); });
const pkiAcmeQueueService = await pkiAcmeQueueServiceFactory({
queueService,
acmeChallengeService
});
const pkiAcmeService = pkiAcmeServiceFactory({ const pkiAcmeService = pkiAcmeServiceFactory({
projectDAL, projectDAL,
appConnectionDAL, appConnectionDAL,
@@ -2253,6 +2301,7 @@ export const registerRoutes = async (
certificateProfileDAL, certificateProfileDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL, certificateSecretDAL,
certificateTemplateV2DAL,
acmeAccountDAL, acmeAccountDAL,
acmeOrderDAL, acmeOrderDAL,
acmeAuthDAL, acmeAuthDAL,
@@ -2262,7 +2311,9 @@ export const registerRoutes = async (
kmsService, kmsService,
licenseService, licenseService,
certificateV3Service, certificateV3Service,
acmeChallengeService certificateTemplateV2Service,
acmeChallengeService,
pkiAcmeQueueService
}); });
const pkiSubscriberService = pkiSubscriberServiceFactory({ const pkiSubscriberService = pkiSubscriberServiceFactory({
@@ -2445,6 +2496,7 @@ export const registerRoutes = async (
await pkiSubscriberQueue.startDailyAutoRenewalJob(); await pkiSubscriberQueue.startDailyAutoRenewalJob();
await pkiAlertV2Queue.init(); await pkiAlertV2Queue.init();
await certificateV3Queue.init(); await certificateV3Queue.init();
await certificateIssuanceQueue.initializeCertificateIssuanceQueue();
await microsoftTeamsService.start(); await microsoftTeamsService.start();
await dynamicSecretQueueService.init(); await dynamicSecretQueueService.init();
await eventBusService.init(); await eventBusService.init();
@@ -2510,6 +2562,7 @@ export const registerRoutes = async (
auditLogStream: auditLogStreamService, auditLogStream: auditLogStreamService,
certificate: certificateService, certificate: certificateService,
certificateV3: certificateV3Service, certificateV3: certificateV3Service,
certificateRequest: certificateRequestService,
certificateEstV3: certificateEstV3Service, certificateEstV3: certificateEstV3Service,
sshCertificateAuthority: sshCertificateAuthorityService, sshCertificateAuthority: sshCertificateAuthorityService,
sshCertificateTemplate: sshCertificateTemplateService, sshCertificateTemplate: sshCertificateTemplateService,
@@ -61,6 +61,10 @@ import {
DigitalOceanConnectionListItemSchema, DigitalOceanConnectionListItemSchema,
SanitizedDigitalOceanConnectionSchema SanitizedDigitalOceanConnectionSchema
} from "@app/services/app-connection/digital-ocean"; } from "@app/services/app-connection/digital-ocean";
import {
DNSMadeEasyConnectionListItemSchema,
SanitizedDNSMadeEasyConnectionSchema
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio"; import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
@@ -175,7 +179,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedRedisConnectionSchema.options, ...SanitizedRedisConnectionSchema.options,
...SanitizedMongoDBConnectionSchema.options, ...SanitizedMongoDBConnectionSchema.options,
...SanitizedLaravelForgeConnectionSchema.options, ...SanitizedLaravelForgeConnectionSchema.options,
...SanitizedChefConnectionSchema.options ...SanitizedChefConnectionSchema.options,
...SanitizedDNSMadeEasyConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -221,7 +226,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
RedisConnectionListItemSchema, RedisConnectionListItemSchema,
MongoDBConnectionListItemSchema, MongoDBConnectionListItemSchema,
LaravelForgeConnectionListItemSchema, LaravelForgeConnectionListItemSchema,
ChefConnectionListItemSchema ChefConnectionListItemSchema,
DNSMadeEasyConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -0,0 +1,51 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateDNSMadeEasyConnectionSchema,
SanitizedDNSMadeEasyConnectionSchema,
UpdateDNSMadeEasyConnectionSchema
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerDNSMadeEasyConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.DNSMadeEasy,
server,
sanitizedResponseSchema: SanitizedDNSMadeEasyConnectionSchema,
createSchema: CreateDNSMadeEasyConnectionSchema,
updateSchema: UpdateDNSMadeEasyConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/dns-made-easy-zones`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const zones = await server.services.appConnection.dnsMadeEasy.listZones(connectionId, req.permission);
return zones;
}
});
};
@@ -16,6 +16,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerChecklyConnectionRouter } from "./checkly-connection-router"; import { registerChecklyConnectionRouter } from "./checkly-connection-router";
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router";
@@ -79,6 +80,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Flyio]: registerFlyioConnectionRouter, [AppConnection.Flyio]: registerFlyioConnectionRouter,
[AppConnection.GitLab]: registerGitLabConnectionRouter, [AppConnection.GitLab]: registerGitLabConnectionRouter,
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter, [AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
[AppConnection.DNSMadeEasy]: registerDNSMadeEasyConnectionRouter,
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter, [AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
[AppConnection.Zabbix]: registerZabbixConnectionRouter, [AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter, [AppConnection.Railway]: registerRailwayConnectionRouter,
@@ -85,7 +85,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
isInternal: false, isInternal: false,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
enableDirectIssuance: !req.body.requireTemplateForIssuance,
...req.body ...req.body
}); });
@@ -220,7 +219,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
isInternal: false, isInternal: false,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
enableDirectIssuance: !req.body.requireTemplateForIssuance,
...req.body ...req.body
}); });
@@ -617,6 +615,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
} }
}); });
// TODO: DEPRECATE
server.route({ server.route({
method: "POST", method: "POST",
url: "/:caId/issue-certificate", url: "/:caId/issue-certificate",
@@ -625,7 +624,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities], tags: [ApiDocsTags.PkiCertificateAuthorities],
description: "Issue certificate from CA", description: "Issue certificate from CA",
params: z.object({ params: z.object({
@@ -711,6 +709,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
} }
}); });
// TODO: DEPRECATE
server.route({ server.route({
method: "POST", method: "POST",
url: "/:caId/sign-certificate", url: "/:caId/sign-certificate",
@@ -719,7 +718,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities], tags: [ApiDocsTags.PkiCertificateAuthorities],
description: "Sign certificate from CA", description: "Sign certificate from CA",
params: z.object({ params: z.object({
@@ -805,6 +803,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
} }
}); });
// TODO: DEPRECATE
server.route({ server.route({
method: "GET", method: "GET",
url: "/:caId/certificate-templates", url: "/:caId/certificate-templates",
@@ -813,7 +812,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities], tags: [ApiDocsTags.PkiCertificateAuthorities],
description: "Get list of certificate templates for the CA", description: "Get list of certificate templates for the CA",
params: z.object({ params: z.object({
@@ -854,6 +852,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
} }
}); });
// TODO: DEPRECATE
server.route({ server.route({
method: "GET", method: "GET",
url: "/:caId/crls", url: "/:caId/crls",
@@ -862,7 +861,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities], tags: [ApiDocsTags.PkiCertificateAuthorities],
description: "Get list of CRLs of the CA", description: "Get list of CRLs of the CA",
params: z.object({ params: z.object({
@@ -28,14 +28,10 @@ export const registerCertificateAuthorityEndpoints = <
projectId: string; projectId: string;
status: CaStatus; status: CaStatus;
configuration: I["configuration"]; configuration: I["configuration"];
enableDirectIssuance: boolean;
}>; }>;
updateSchema: z.ZodType<{ updateSchema: z.ZodType<{
projectId: string;
name?: string;
status?: CaStatus; status?: CaStatus;
configuration?: I["configuration"]; configuration?: I["configuration"];
enableDirectIssuance?: boolean;
}>; }>;
responseSchema: z.ZodTypeAny; responseSchema: z.ZodTypeAny;
}) => { }) => {
@@ -83,7 +79,7 @@ export const registerCertificateAuthorityEndpoints = <
server.route({ server.route({
method: "GET", method: "GET",
url: "/:caName", url: "/:id",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -91,10 +87,7 @@ export const registerCertificateAuthorityEndpoints = <
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities], tags: [ApiDocsTags.PkiCertificateAuthorities],
params: z.object({ params: z.object({
caName: z.string() id: z.string()
}),
querystring: z.object({
projectId: z.string().uuid()
}), }),
response: { response: {
200: responseSchema 200: responseSchema
@@ -102,14 +95,12 @@ export const registerCertificateAuthorityEndpoints = <
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { caName } = req.params; const { id } = req.params;
const { projectId } = req.query;
const certificateAuthority = const certificateAuthority = (await server.services.certificateAuthority.findCertificateAuthorityById(
(await server.services.certificateAuthority.findCertificateAuthorityByNameAndProjectId( { id, type: caType },
{ caName, type: caType, projectId }, req.permission
req.permission )) as T;
)) as T;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
@@ -166,7 +157,7 @@ export const registerCertificateAuthorityEndpoints = <
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/:caName", url: "/:id",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -174,7 +165,7 @@ export const registerCertificateAuthorityEndpoints = <
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities], tags: [ApiDocsTags.PkiCertificateAuthorities],
params: z.object({ params: z.object({
caName: z.string() id: z.string()
}), }),
body: updateSchema, body: updateSchema,
response: { response: {
@@ -183,13 +174,13 @@ export const registerCertificateAuthorityEndpoints = <
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { caName } = req.params; const { id } = req.params;
const certificateAuthority = (await server.services.certificateAuthority.updateCertificateAuthority( const certificateAuthority = (await server.services.certificateAuthority.updateCertificateAuthority(
{ {
...req.body, ...req.body,
type: caType, type: caType,
caName id
}, },
req.permission req.permission
)) as T; )) as T;
@@ -213,7 +204,7 @@ export const registerCertificateAuthorityEndpoints = <
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:caName", url: "/:id",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -221,10 +212,7 @@ export const registerCertificateAuthorityEndpoints = <
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities], tags: [ApiDocsTags.PkiCertificateAuthorities],
params: z.object({ params: z.object({
caName: z.string() id: z.string()
}),
body: z.object({
projectId: z.string().uuid()
}), }),
response: { response: {
200: responseSchema 200: responseSchema
@@ -232,11 +220,10 @@ export const registerCertificateAuthorityEndpoints = <
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { caName } = req.params; const { id } = req.params;
const { projectId } = req.body;
const certificateAuthority = (await server.services.certificateAuthority.deleteCertificateAuthority( const certificateAuthority = (await server.services.certificateAuthority.deleteCertificateAuthority(
{ caName, type: caType, projectId }, { id, type: caType },
req.permission req.permission
)) as T; )) as T;
@@ -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 ?? [])]
};
}
});
};
@@ -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 { import {
CreateInternalCertificateAuthoritySchema, CreateInternalCertificateAuthoritySchema,
InternalCertificateAuthoritySchema, InternalCertificateAuthoritySchema,
@@ -15,4 +23,406 @@ export const registerInternalCertificateAuthorityRouter = async (server: Fastify
createSchema: CreateInternalCertificateAuthoritySchema, createSchema: CreateInternalCertificateAuthoritySchema,
updateSchema: UpdateInternalCertificateAuthoritySchema 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);
}
});
}; };
@@ -8,7 +8,8 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CertStatus } from "@app/services/certificate/certificate-types"; import { CertStatus } from "@app/services/certificate/certificate-types";
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; import { 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) => { export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -23,7 +24,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
body: z body: z
.object({ .object({
projectId: z.string().min(1), projectId: z.string().min(1),
caId: z.string().uuid(), caId: z.string().uuid().optional(),
certificateTemplateId: z.string().uuid(), certificateTemplateId: z.string().uuid(),
slug: z slug: z
.string() .string()
@@ -32,6 +33,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"), .regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
description: z.string().max(1000).optional(), description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType), enrollmentType: z.nativeEnum(EnrollmentType),
issuerType: z.nativeEnum(IssuerType).default(IssuerType.CA),
estConfig: z estConfig: z
.object({ .object({
disableBootstrapCaValidation: z.boolean().default(false), disableBootstrapCaValidation: z.boolean().default(false),
@@ -45,53 +47,113 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
renewBeforeDays: z.number().min(1).max(30).optional() renewBeforeDays: z.number().min(1).max(30).optional()
}) })
.optional(), .optional(),
acmeConfig: z.object({}).optional() acmeConfig: z.object({}).optional(),
externalConfigs: ExternalConfigUnionSchema
}) })
.refine( .refine(
(data) => { (data) => {
if (data.enrollmentType === EnrollmentType.EST) { if (data.enrollmentType === EnrollmentType.EST) {
if (!data.estConfig) { return !!data.estConfig;
return false;
}
if (data.apiConfig) {
return false;
}
if (data.acmeConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.API) {
if (!data.apiConfig) {
return false;
}
if (data.estConfig) {
return false;
}
if (data.acmeConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.ACME) {
if (!data.acmeConfig) {
return false;
}
if (data.estConfig) {
return false;
}
if (data.apiConfig) {
return false;
}
} }
return true; return true;
}, },
{ {
message: message: "EST enrollment type requires EST configuration"
"EST enrollment type requires EST configuration and cannot have API or ACME configuration. API enrollment type requires API configuration and cannot have EST or ACME configuration. ACME enrollment type requires ACME configuration and cannot have EST or API configuration." }
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !!data.apiConfig;
}
return true;
},
{
message: "API enrollment type requires API configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !!data.acmeConfig;
}
return true;
},
{
message: "ACME enrollment type requires ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
return !data.apiConfig && !data.acmeConfig;
}
return true;
},
{
message: "EST enrollment type cannot have API or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.API) {
return !data.estConfig && !data.acmeConfig;
}
return true;
},
{
message: "API enrollment type cannot have EST or ACME configuration"
}
)
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.ACME) {
return !data.estConfig && !data.apiConfig;
}
return true;
},
{
message: "ACME enrollment type cannot have EST or API configuration"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.CA) {
return !!data.caId;
}
return true;
},
{
message: "CA issuer type requires a CA ID"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return !data.caId;
}
return true;
},
{
message: "Self-signed issuer type cannot have a CA ID"
}
)
.refine(
(data) => {
if (data.issuerType === IssuerType.SELF_SIGNED) {
return data.enrollmentType === EnrollmentType.API;
}
return true;
},
{
message: "Self-signed issuer type only supports API enrollment"
} }
), ),
response: { response: {
200: z.object({ 200: z.object({
certificateProfile: PkiCertificateProfilesSchema certificateProfile: PkiCertificateProfilesSchema.extend({
externalConfigs: ExternalConfigUnionSchema
})
}) })
} }
}, },
@@ -115,7 +177,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
certificateProfileId: certificateProfile.id, certificateProfileId: certificateProfile.id,
name: certificateProfile.slug, name: certificateProfile.slug,
projectId: certificateProfile.projectId, projectId: certificateProfile.projectId,
enrollmentType: certificateProfile.enrollmentType enrollmentType: certificateProfile.enrollmentType,
issuerType: certificateProfile.issuerType
} }
} }
}); });
@@ -139,11 +202,21 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
limit: z.coerce.number().min(1).max(100).default(20), limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional(), search: z.string().optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(), enrollmentType: z.nativeEnum(EnrollmentType).optional(),
issuerType: z.nativeEnum(IssuerType).optional(),
caId: z.string().uuid().optional() caId: z.string().uuid().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
certificateProfiles: PkiCertificateProfilesSchema.extend({ 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 metrics: z
.object({ .object({
profileId: z.string(), profileId: z.string(),
@@ -174,7 +247,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
id: z.string(), id: z.string(),
directoryUrl: z.string() directoryUrl: z.string()
}) })
.optional() .optional(),
externalConfigs: ExternalConfigUnionSchema
}).array(), }).array(),
totalCount: z.number() totalCount: z.number()
}) })
@@ -220,12 +294,16 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
response: { response: {
200: z.object({ 200: z.object({
certificateProfile: PkiCertificateProfilesSchema.extend({ certificateProfile: PkiCertificateProfilesSchema.extend({
externalConfigs: ExternalConfigUnionSchema
}).extend({
certificateAuthority: z certificateAuthority: z
.object({ .object({
id: z.string(), id: z.string(),
projectId: z.string(), projectId: z.string(),
status: z.string(), status: z.string(),
name: z.string() name: z.string(),
isExternal: z.boolean().optional(),
externalType: z.string().nullable().optional()
}) })
.optional(), .optional(),
certificateTemplate: z certificateTemplate: z
@@ -250,7 +328,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
autoRenew: z.boolean(), autoRenew: z.boolean(),
renewBeforeDays: z.number().optional() renewBeforeDays: z.number().optional()
}) })
.optional() .optional(),
externalConfigs: ExternalConfigUnionSchema
}) })
}) })
} }
@@ -298,7 +377,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
}), }),
response: { response: {
200: z.object({ 200: z.object({
certificateProfile: PkiCertificateProfilesSchema certificateProfile: PkiCertificateProfilesSchema.extend({
externalConfigs: ExternalConfigUnionSchema
})
}) })
} }
}, },
@@ -339,6 +420,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
.optional(), .optional(),
description: z.string().max(1000).optional(), description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(), enrollmentType: z.nativeEnum(EnrollmentType).optional(),
issuerType: z.nativeEnum(IssuerType).optional(),
estConfig: z estConfig: z
.object({ .object({
disableBootstrapCaValidation: z.boolean().default(false), disableBootstrapCaValidation: z.boolean().default(false),
@@ -351,7 +433,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
autoRenew: z.boolean().default(false), autoRenew: z.boolean().default(false),
renewBeforeDays: z.number().min(1).max(30).optional() renewBeforeDays: z.number().min(1).max(30).optional()
}) })
.optional() .optional(),
externalConfigs: ExternalConfigUnionSchema
}) })
.refine( .refine(
(data) => { (data) => {
@@ -373,7 +456,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
), ),
response: { response: {
200: z.object({ 200: z.object({
certificateProfile: PkiCertificateProfilesSchema certificateProfile: PkiCertificateProfilesSchema.extend({
externalConfigs: ExternalConfigUnionSchema
})
}) })
} }
}, },
@@ -418,7 +503,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
}), }),
response: { response: {
200: z.object({ 200: z.object({
certificateProfile: PkiCertificateProfilesSchema certificateProfile: PkiCertificateProfilesSchema.extend({
externalConfigs: ExternalConfigUnionSchema
})
}) })
} }
}, },
File diff suppressed because it is too large Load Diff
@@ -1,28 +1,239 @@
import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; import { ApiDocsTags } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; 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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; import {
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema"; CertExtendedKeyUsageType,
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; 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({ const attributeTypeSchema = z.nativeEnum(CertSubjectAttributeType);
id: true, const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
certificateTemplateId: true,
isEnabled: true, const templateV2SubjectSchema = z
disableBootstrapCertValidation: true .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) => { 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({ server.route({
method: "GET", 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: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -30,20 +241,22 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates], tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({ params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.GET.certificateTemplateId) id: z.string().uuid()
}), }),
response: { response: {
200: sanitizedCertificateTemplate 200: z.object({
certificateTemplate: certificateTemplateV2ResponseSchema
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.getCertTemplate({ const certificateTemplate = await server.services.certificateTemplateV2.getTemplateV2ById({
id: req.params.certificateTemplateId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod!,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId,
templateId: req.params.id
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -58,125 +271,38 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
} }
}); });
return certificateTemplate; 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({ server.route({
method: "PATCH", method: "PATCH",
url: "/:certificateTemplateId", url: "/:id",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates], 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({ params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId) id: z.string().uuid()
}), }),
body: updateCertificateTemplateV2Schema,
response: { response: {
200: sanitizedCertificateTemplate 200: z.object({
certificateTemplate: certificateTemplateV2ResponseSchema
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.updateCertTemplate({ const certificateTemplate = await server.services.certificateTemplateV2.updateTemplateV2({
...req.body,
id: req.params.certificateTemplateId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod!,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId,
templateId: req.params.id,
data: req.body
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -186,23 +312,18 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
type: EventType.UPDATE_CERTIFICATE_TEMPLATE, type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
metadata: { metadata: {
certificateTemplateId: certificateTemplate.id, certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name, name: certificateTemplate.name
caId: certificateTemplate.caId,
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
commonName: certificateTemplate.commonName,
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
ttl: certificateTemplate.ttl
} }
} }
}); });
return certificateTemplate; return { certificateTemplate };
} }
}); });
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:certificateTemplateId", url: "/:id",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -210,20 +331,22 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates], tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({ params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId) id: z.string().uuid()
}), }),
response: { response: {
200: sanitizedCertificateTemplate 200: z.object({
certificateTemplate: certificateTemplateV2ResponseSchema
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplate.deleteCertTemplate({ const certificateTemplate = await server.services.certificateTemplateV2.deleteTemplateV2({
id: req.params.certificateTemplateId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod!,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId,
templateId: req.params.id
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -238,158 +361,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
} }
}); });
return certificateTemplate; 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;
} }
}); });
}; };
@@ -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
});
};
@@ -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 };
}
});
};
@@ -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;
}
});
};
@@ -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<void>
> = {
[CaType.INTERNAL]: registerInternalCertificateAuthorityRouter,
[CaType.ACME]: registerAcmeCertificateAuthorityRouter,
[CaType.AZURE_AD_CS]: registerAzureAdCsCertificateAuthorityRouter
};
@@ -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
});
};
@@ -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;
}
});
};
@@ -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;
}
});
};
@@ -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;
}
});
};
@@ -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({ server.route({
method: "GET", method: "GET",
url: "/token-auth/identities/:identityId/tokens/:tokenId", 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]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false, hide: true,
tags: [ApiDocsTags.TokenAuth], tags: [ApiDocsTags.TokenAuth],
description: "Get token for machine identity with Token Auth", description: "Get token for machine identity with Token Auth",
security: [ security: [
@@ -436,13 +437,11 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
}, },
handler: async (req) => { handler: async (req) => {
const { token, identityMembershipOrg } = await server.services.identityTokenAuth.getTokenAuthTokenById({ const { token, identityMembershipOrg } = await server.services.identityTokenAuth.getTokenAuthTokenById({
identityId: req.params.identityId,
tokenId: req.params.tokenId, tokenId: req.params.tokenId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod
isActorSuperAdmin: isSuperAdmin(req.auth)
}); });
await server.services.auditLog.createAuditLog({ 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({ server.route({
method: "PATCH", method: "PATCH",
url: "/token-auth/tokens/:tokenId", url: "/token-auth/tokens/:tokenId",
+43 -5
View File
@@ -11,10 +11,15 @@ import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router"; import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router"; import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers"; 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 { registerCertificateProfilesRouter } from "./certificate-profiles-router";
import { registerCertRouter } from "./certificate-router"; import { registerCertificateRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-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 { registerDeprecatedIdentityProjectMembershipRouter } from "./deprecated-identity-project-membership-router";
import { registerDeprecatedPkiAlertRouter } from "./deprecated-pki-alert-router";
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router"; import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router"; import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
import { registerDeprecatedProjectRouter } from "./deprecated-project-router"; import { registerDeprecatedProjectRouter } from "./deprecated-project-router";
@@ -150,21 +155,54 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register( await server.register(
async (pkiRouter) => { async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
await pkiRouter.register( await pkiRouter.register(
async (caRouter) => { async (caRouter) => {
for await (const [caType, router] of Object.entries(CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP)) { for await (const [caType, router] of Object.entries(CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP)) {
await caRouter.register(router, { prefix: `/${caType}` }); 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" prefix: "/ca"
} }
); );
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" }); await pkiRouter.register(registerDeprecatedCertRouter, { prefix: "/certificates" });
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" }); await pkiRouter.register(registerDeprecatedCertificateTemplateRouter, { prefix: "/certificate-templates" });
await pkiRouter.register(registerCertificateProfilesRouter, { prefix: "/certificate-profiles" }); 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(registerPkiCollectionRouter, { prefix: "/collections" });
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" }); await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
await pkiRouter.register( await pkiRouter.register(
@@ -10,7 +10,12 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema"; import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
import { Integrations } from "@app/services/integration-auth/integration-list"; 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"; import {} from "../sanitizedSchemas";
@@ -288,31 +293,47 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: integration.projectId, projectId: integration.projectId,
event: { event: {
type: EventType.DELETE_INTEGRATION, type: EventType.DELETE_INTEGRATION,
// eslint-disable-next-line // eslint-disable-next-line
metadata: shake({ metadata: {
integrationId: integration.id, ...deleteIntegrationEventProperty,
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,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
// eslint-disable-next-line // 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 }; return { integration };
} }
}); });
@@ -351,28 +372,41 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
id: req.params.integrationId 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: integration.projectId, projectId: integration.projectId,
event: { event: {
type: EventType.MANUAL_SYNC_INTEGRATION, type: EventType.MANUAL_SYNC_INTEGRATION,
// eslint-disable-next-line // eslint-disable-next-line
metadata: shake({ metadata: syncIntegrationEventProperty as any
integrationId: integration.id, }
integration: integration.integration, });
environment: integration.environment.slug,
secretPath: integration.secretPath, await server.services.telemetry.sendPostHogEvents({
url: integration.url, event: PostHogEventTypes.IntegrationSynced,
app: integration.app, organizationId: req.permission.orgId,
appId: integration.appId, distinctId: getTelemetryDistinctId(req),
targetEnvironment: integration.targetEnvironment, properties: {
targetEnvironmentId: integration.targetEnvironmentId, ...syncIntegrationEventProperty,
targetService: integration.targetService, projectId: integration.projectId,
targetServiceId: integration.targetServiceId, isManualSync: true,
path: integration.path, ...req.auditLogInfo
region: integration.region
// eslint-disable-next-line
}) as any
} }
}); });
+298 -53
View File
@@ -1,12 +1,18 @@
import { z } from "zod"; import { z } from "zod";
import { PkiAlertsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; 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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { 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) => { export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -17,25 +23,41 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
description: "Create a new PKI alert",
tags: [ApiDocsTags.PkiAlerting], tags: [ApiDocsTags.PkiAlerting],
description: "Create PKI alert", body: CreatePkiAlertV2Schema.extend({
body: z.object({ projectId: z.string().uuid().describe("Project ID")
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: { 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) => { handler: async (req) => {
const alert = await server.services.pkiAlert.createPkiAlert({ const alert = await server.services.pkiAlertV2.createAlert({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -45,21 +67,80 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: alert.projectId, projectId: req.body.projectId,
event: { event: {
type: EventType.CREATE_PKI_ALERT, type: EventType.CREATE_PKI_ALERT,
metadata: { metadata: {
pkiAlertId: alert.id, pkiAlertId: alert.id,
pkiCollectionId: alert.pkiCollectionId,
name: alert.name, name: alert.name,
alertBefore: alert.alertBeforeDays.toString(), eventType: alert.eventType,
eventType: PkiAlertEventType.EXPIRATION, alertBefore: alert.alertBefore
recipientEmails: alert.recipientEmails
} }
} }
}); });
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]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
description: "Get a PKI alert by ID",
tags: [ApiDocsTags.PkiAlerting], tags: [ApiDocsTags.PkiAlerting],
description: "Get PKI alert",
params: z.object({ params: z.object({
alertId: z.string().trim().describe(ALERTS.GET.alertId) alertId: z.string().uuid().describe("Alert ID")
}), }),
response: { 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) => { handler: async (req) => {
const alert = await server.services.pkiAlert.getPkiAlertById({ const alert = await server.services.pkiAlertV2.getAlertById({
alertId: req.params.alertId, alertId: req.params.alertId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, 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", method: "PATCH",
url: "/:alertId", url: "/:alertId",
config: { config: {
rateLimit: readLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
description: "Update a PKI alert",
tags: [ApiDocsTags.PkiAlerting], tags: [ApiDocsTags.PkiAlerting],
description: "Update PKI alert",
params: z.object({ params: z.object({
alertId: z.string().trim().describe(ALERTS.UPDATE.alertId) alertId: z.string().uuid().describe("Alert ID")
}),
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)
}), }),
body: UpdatePkiAlertV2Schema,
response: { 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) => { handler: async (req) => {
const alert = await server.services.pkiAlert.updatePkiAlert({ const alert = await server.services.pkiAlertV2.updateAlert({
alertId: req.params.alertId, alertId: req.params.alertId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -149,16 +268,14 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
type: EventType.UPDATE_PKI_ALERT, type: EventType.UPDATE_PKI_ALERT,
metadata: { metadata: {
pkiAlertId: alert.id, pkiAlertId: alert.id,
pkiCollectionId: alert.pkiCollectionId,
name: alert.name, name: alert.name,
alertBefore: alert.alertBeforeDays.toString(), eventType: alert.eventType,
eventType: PkiAlertEventType.EXPIRATION, alertBefore: alert.alertBefore
recipientEmails: alert.recipientEmails
} }
} }
}); });
return alert; return { alert };
} }
}); });
@@ -170,17 +287,41 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
hide: false,
description: "Delete a PKI alert",
tags: [ApiDocsTags.PkiAlerting], tags: [ApiDocsTags.PkiAlerting],
description: "Delete PKI alert",
params: z.object({ params: z.object({
alertId: z.string().trim().describe(ALERTS.DELETE.alertId) alertId: z.string().uuid().describe("Alert ID")
}), }),
response: { 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) => { handler: async (req) => {
const alert = await server.services.pkiAlert.deletePkiAlert({ const alert = await server.services.pkiAlertV2.deleteAlert({
alertId: req.params.alertId, alertId: req.params.alertId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, 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;
} }
}); });
}; };
+1 -1
View File
@@ -1,5 +1,5 @@
import { registerCaRouter } from "./certificate-authority-router"; 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 { registerDeprecatedGroupProjectRouter } from "./deprecated-group-project-router";
import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-router"; import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-router";
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router"; import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
@@ -2,16 +2,12 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs"; import { ApiDocsTags } from "@app/lib/api-docs";
import { NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { import { CertKeyAlgorithm, CertSignatureAlgorithm } from "@app/services/certificate/certificate-types";
ACMESANType,
CertificateOrderStatus,
CertKeyAlgorithm,
CertSignatureAlgorithm
} from "@app/services/certificate/certificate-types";
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators"; import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
import { import {
CertExtendedKeyUsageType, CertExtendedKeyUsageType,
@@ -21,6 +17,7 @@ import {
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils"; import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils"; import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; 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 { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
import { booleanSchema } from "../sanitizedSchemas"; import { booleanSchema } from "../sanitizedSchemas";
@@ -65,8 +62,10 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
hide: false, hide: true,
deprecated: true,
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
description: "This endpoint will be removed in a future version.",
body: z body: z
.object({ .object({
profileId: z.string().uuid(), profileId: z.string().uuid(),
@@ -106,7 +105,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
certificateChain: z.string().trim(), certificateChain: z.string().trim(),
privateKey: z.string().trim().optional(), privateKey: z.string().trim().optional(),
serialNumber: z.string().trim(), 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 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: data.projectId, 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 rateLimit: writeLimit
}, },
schema: { schema: {
hide: false, hide: true,
deprecated: true,
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
description: "This endpoint will be removed in a future version.",
body: z body: z
.object({ .object({
profileId: z.string().uuid(), profileId: z.string().uuid(),
@@ -191,14 +219,13 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
issuingCaCertificate: z.string().trim(), issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(), certificateChain: z.string().trim(),
serialNumber: z.string().trim(), serialNumber: z.string().trim(),
certificateId: z.string() certificateId: z.string(),
certificateRequestId: z.string()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const certificateRequest = extractCertificateRequestFromCSR(req.body.csr);
const data = await server.services.certificateV3.signCertificateFromProfile({ const data = await server.services.certificateV3.signCertificateFromProfile({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -215,6 +242,32 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
removeRootsFromChain: req.body.removeRootsFromChain 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: data.projectId, projectId: data.projectId,
@@ -224,12 +277,15 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
certificateProfileId: req.body.profileId, certificateProfileId: req.body.profileId,
certificateId: data.certificateId, certificateId: data.certificateId,
profileName: data.profileName, 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 rateLimit: writeLimit
}, },
schema: { schema: {
hide: false, hide: true,
deprecated: true,
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
description: "This endpoint will be removed in a future version.",
body: z body: z
.object({ .object({
profileId: z.string().uuid(), profileId: z.string().uuid(),
subjectAlternativeNames: z subjectAlternativeNames: z.array(
.array( z.object({
z.object({ type: z.nativeEnum(CertSubjectAlternativeNameType),
type: z.nativeEnum(ACMESANType), value: z
value: z .string()
.string() .trim()
.trim() .min(1, "SAN value cannot be empty")
.min(1, "SAN value cannot be empty") .max(255, "SAN value must be less than 255 characters")
.max(255, "SAN value must be less than 255 characters") })
}) ),
)
.min(1, "At least one subject alternative name must be provided"),
ttl: z ttl: z
.string() .string()
.trim() .trim()
@@ -280,62 +336,55 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
}), }),
response: { response: {
200: z.object({ 200: z.object({
orderId: z.string(), certificate: z.string().optional(),
status: z.nativeEnum(CertificateOrderStatus), certificateRequestId: z.string()
subjectAlternativeNames: z.array(
z.object({
type: z.nativeEnum(ACMESANType),
value: z.string(),
status: z.nativeEnum(CertificateOrderStatus)
})
),
authorizations: z.array(
z.object({
identifier: z.object({
type: z.nativeEnum(ACMESANType),
value: z.string()
}),
status: z.nativeEnum(CertificateOrderStatus),
expires: z.string().optional(),
challenges: z.array(
z.object({
type: z.string(),
status: z.nativeEnum(CertificateOrderStatus),
url: z.string(),
token: z.string()
})
)
})
),
finalize: z.string(),
certificate: z.string().optional()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { 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({ const data = await server.services.certificateV3.orderCertificateFromProfile({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
profileId: req.body.profileId, profileId: req.body.profileId,
certificateOrder: { certificateOrder: 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
},
removeRootsFromChain: req.body.removeRootsFromChain 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: data.projectId, projectId: data.projectId,
@@ -343,13 +392,15 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE, type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
metadata: { metadata: {
certificateProfileId: req.body.profileId, certificateProfileId: req.body.profileId,
orderId: data.orderId,
profileName: data.profileName profileName: data.profileName
} }
} }
}); });
return data; return {
...data,
certificateRequestId: certificateRequest.id
};
} }
}); });
@@ -377,12 +428,24 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
certificateChain: z.string().trim(), certificateChain: z.string().trim(),
privateKey: z.string().trim().optional(), privateKey: z.string().trim().optional(),
serialNumber: z.string().trim(), serialNumber: z.string().trim(),
certificateId: z.string() certificateId: z.string(),
certificateRequestId: z.string()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { 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({ const data = await server.services.certificateV3.renewCertificate({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
+1 -1
View File
@@ -1,4 +1,4 @@
import { registerCertificatesRouter } from "./certificates-router"; import { registerCertificatesRouter } from "./deprecated-certificates-router";
import { registerDeprecatedSecretRouter } from "./deprecated-secret-router"; import { registerDeprecatedSecretRouter } from "./deprecated-secret-router";
import { registerExternalMigrationRouter } from "./external-migration-router"; import { registerExternalMigrationRouter } from "./external-migration-router";
import { registerLoginRouter } from "./login-router"; import { registerLoginRouter } from "./login-router";
@@ -29,6 +29,7 @@ export enum AppConnection {
Flyio = "flyio", Flyio = "flyio",
GitLab = "gitlab", GitLab = "gitlab",
Cloudflare = "cloudflare", Cloudflare = "cloudflare",
DNSMadeEasy = "dns-made-easy",
Zabbix = "zabbix", Zabbix = "zabbix",
Railway = "railway", Railway = "railway",
Bitbucket = "bitbucket", Bitbucket = "bitbucket",
@@ -88,6 +88,11 @@ import {
getDigitalOceanConnectionListItem, getDigitalOceanConnectionListItem,
validateDigitalOceanConnectionCredentials validateDigitalOceanConnectionCredentials
} from "./digital-ocean"; } from "./digital-ocean";
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy/dns-made-easy-connection-enum";
import {
getDNSMadeEasyConnectionListItem,
validateDNSMadeEasyConnectionCredentials
} from "./dns-made-easy/dns-made-easy-connection-fns";
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio"; import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp"; import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github"; import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
@@ -172,7 +177,8 @@ const PKI_APP_CONNECTIONS = [
AppConnection.Cloudflare, AppConnection.Cloudflare,
AppConnection.AzureADCS, AppConnection.AzureADCS,
AppConnection.AzureKeyVault, AppConnection.AzureKeyVault,
AppConnection.Chef AppConnection.Chef,
AppConnection.DNSMadeEasy
]; ];
export const listAppConnectionOptions = (projectType?: ProjectType) => { export const listAppConnectionOptions = (projectType?: ProjectType) => {
@@ -208,6 +214,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getFlyioConnectionListItem(), getFlyioConnectionListItem(),
getGitLabConnectionListItem(), getGitLabConnectionListItem(),
getCloudflareConnectionListItem(), getCloudflareConnectionListItem(),
getDNSMadeEasyConnectionListItem(),
getZabbixConnectionListItem(), getZabbixConnectionListItem(),
getRailwayConnectionListItem(), getRailwayConnectionListItem(),
getBitbucketConnectionListItem(), getBitbucketConnectionListItem(),
@@ -341,6 +348,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.DNSMadeEasy]: validateDNSMadeEasyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
@@ -398,6 +406,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case OktaConnectionMethod.ApiToken: case OktaConnectionMethod.ApiToken:
case LaravelForgeConnectionMethod.ApiToken: case LaravelForgeConnectionMethod.ApiToken:
return "API Token"; return "API Token";
case DNSMadeEasyConnectionMethod.APIKeySecret:
return "API Key & Secret";
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
case MySqlConnectionMethod.UsernameAndPassword: case MySqlConnectionMethod.UsernameAndPassword:
@@ -487,6 +497,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.Flyio]: platformManagedCredentialsNotSupported,
[AppConnection.GitLab]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported,
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
[AppConnection.DNSMadeEasy]: platformManagedCredentialsNotSupported,
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported, [AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
[AppConnection.Railway]: platformManagedCredentialsNotSupported, [AppConnection.Railway]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
@@ -32,6 +32,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Flyio]: "Fly.io", [AppConnection.Flyio]: "Fly.io",
[AppConnection.GitLab]: "GitLab", [AppConnection.GitLab]: "GitLab",
[AppConnection.Cloudflare]: "Cloudflare", [AppConnection.Cloudflare]: "Cloudflare",
[AppConnection.DNSMadeEasy]: "DNS Made Easy",
[AppConnection.Zabbix]: "Zabbix", [AppConnection.Zabbix]: "Zabbix",
[AppConnection.Railway]: "Railway", [AppConnection.Railway]: "Railway",
[AppConnection.Bitbucket]: "Bitbucket", [AppConnection.Bitbucket]: "Bitbucket",
@@ -78,6 +79,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Flyio]: AppConnectionPlanType.Regular, [AppConnection.Flyio]: AppConnectionPlanType.Regular,
[AppConnection.GitLab]: AppConnectionPlanType.Regular, [AppConnection.GitLab]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular, [AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
[AppConnection.DNSMadeEasy]: AppConnectionPlanType.Regular,
[AppConnection.Zabbix]: AppConnectionPlanType.Regular, [AppConnection.Zabbix]: AppConnectionPlanType.Regular,
[AppConnection.Railway]: AppConnectionPlanType.Regular, [AppConnection.Railway]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular, [AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
@@ -72,6 +72,8 @@ import { checklyConnectionService } from "./checkly/checkly-connection-service";
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema"; import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service"; import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks"; import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
import { ValidateDNSMadeEasyConnectionCredentialsSchema } from "./dns-made-easy/dns-made-easy-connection-schema";
import { dnsMadeEasyConnectionService } from "./dns-made-easy/dns-made-easy-connection-service";
import { databricksConnectionService } from "./databricks/databricks-connection-service"; import { databricksConnectionService } from "./databricks/databricks-connection-service";
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean"; import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service"; import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
@@ -168,6 +170,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema, [AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema, [AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema, [AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
[AppConnection.DNSMadeEasy]: ValidateDNSMadeEasyConnectionCredentialsSchema,
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema, [AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema, [AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema, [AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
@@ -877,6 +880,7 @@ export const appConnectionServiceFactory = ({
flyio: flyioConnectionService(connectAppConnectionById), flyio: flyioConnectionService(connectAppConnectionById),
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
cloudflare: cloudflareConnectionService(connectAppConnectionById), cloudflare: cloudflareConnectionService(connectAppConnectionById),
dnsMadeEasy: dnsMadeEasyConnectionService(connectAppConnectionById),
zabbix: zabbixConnectionService(connectAppConnectionById), zabbix: zabbixConnectionService(connectAppConnectionById),
railway: railwayConnectionService(connectAppConnectionById), railway: railwayConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById), bitbucket: bitbucketConnectionService(connectAppConnectionById),
@@ -15,8 +15,8 @@ import {
TOracleDBConnectionInput, TOracleDBConnectionInput,
TValidateOracleDBConnectionCredentialsSchema TValidateOracleDBConnectionCredentialsSchema
} from "@app/ee/services/app-connections/oracledb"; } from "@app/ee/services/app-connections/oracledb";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@@ -106,6 +106,12 @@ import {
TDigitalOceanConnectionInput, TDigitalOceanConnectionInput,
TValidateDigitalOceanCredentialsSchema TValidateDigitalOceanCredentialsSchema
} from "./digital-ocean"; } from "./digital-ocean";
import {
TDNSMadeEasyConnection,
TDNSMadeEasyConnectionConfig,
TDNSMadeEasyConnectionInput,
TValidateDNSMadeEasyConnectionCredentialsSchema
} from "./dns-made-easy/dns-made-easy-connection-types";
import { import {
TFlyioConnection, TFlyioConnection,
TFlyioConnectionConfig, TFlyioConnectionConfig,
@@ -285,6 +291,7 @@ export type TAppConnection = { id: string } & (
| TGitLabConnection | TGitLabConnection
| TCloudflareConnection | TCloudflareConnection
| TBitbucketConnection | TBitbucketConnection
| TDNSMadeEasyConnection
| TZabbixConnection | TZabbixConnection
| TRailwayConnection | TRailwayConnection
| TChecklyConnection | TChecklyConnection
@@ -335,6 +342,7 @@ export type TAppConnectionInput = { id: string } & (
| TGitLabConnectionInput | TGitLabConnectionInput
| TCloudflareConnectionInput | TCloudflareConnectionInput
| TBitbucketConnectionInput | TBitbucketConnectionInput
| TDNSMadeEasyConnectionInput
| TZabbixConnectionInput | TZabbixConnectionInput
| TRailwayConnectionInput | TRailwayConnectionInput
| TChecklyConnectionInput | TChecklyConnectionInput
@@ -403,6 +411,7 @@ export type TAppConnectionConfig =
| TGitLabConnectionConfig | TGitLabConnectionConfig
| TCloudflareConnectionConfig | TCloudflareConnectionConfig
| TBitbucketConnectionConfig | TBitbucketConnectionConfig
| TDNSMadeEasyConnectionConfig
| TZabbixConnectionConfig | TZabbixConnectionConfig
| TRailwayConnectionConfig | TRailwayConnectionConfig
| TChecklyConnectionConfig | TChecklyConnectionConfig
@@ -448,6 +457,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateGitLabConnectionCredentialsSchema | TValidateGitLabConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema | TValidateCloudflareConnectionCredentialsSchema
| TValidateBitbucketConnectionCredentialsSchema | TValidateBitbucketConnectionCredentialsSchema
| TValidateDNSMadeEasyConnectionCredentialsSchema
| TValidateZabbixConnectionCredentialsSchema | TValidateZabbixConnectionCredentialsSchema
| TValidateRailwayConnectionCredentialsSchema | TValidateRailwayConnectionCredentialsSchema
| TValidateChecklyConnectionCredentialsSchema | TValidateChecklyConnectionCredentialsSchema
@@ -0,0 +1,3 @@
export enum DNSMadeEasyConnectionMethod {
APIKeySecret = "api-key-secret"
}
@@ -0,0 +1,221 @@
import { AxiosError } from "axios";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
import {
TDNSMadeEasyConnection,
TDNSMadeEasyConnectionConfig,
TDNSMadeEasyZone
} from "./dns-made-easy-connection-types";
interface DNSMadeEasyApiResponse {
totalRecords: number;
totalPages: number;
data: Array<{
id: number;
name: string;
type: string;
value: string;
}>;
page: number;
}
export const getDNSMadeEasyUrl = (path: string) => {
const appCfg = getConfig();
return `${appCfg.DNS_MADE_EASY_SANDBOX_ENABLED ? IntegrationUrls.DNS_MADE_EASY_SANDBOX_API_URL : IntegrationUrls.DNS_MADE_EASY_API_URL}${path}`;
};
export const makeDNSMadeEasyAuthHeaders = (
apiKey: string,
secretKey: string,
currentDate?: Date
): Record<string, string> => {
// Format date as "Day, DD Mon YYYY HH:MM:SS GMT" (e.g., "Mon, 01 Jan 2024 12:00:00 GMT")
const requestDate = (currentDate ?? new Date()).toUTCString();
// Generate HMAC-SHA1 signature
const hmac = crypto.nativeCrypto.createHmac("sha1", secretKey);
hmac.update(requestDate);
const hmacSignature = hmac.digest("hex");
return {
"x-dnsme-apiKey": apiKey,
"x-dnsme-hmac": hmacSignature,
"x-dnsme-requestDate": requestDate
};
};
export const getDNSMadeEasyConnectionListItem = () => {
return {
name: "DNS Made Easy" as const,
app: AppConnection.DNSMadeEasy as const,
methods: Object.values(DNSMadeEasyConnectionMethod) as [DNSMadeEasyConnectionMethod.APIKeySecret]
};
};
export const listDNSMadeEasyZones = async (appConnection: TDNSMadeEasyConnection): Promise<TDNSMadeEasyZone[]> => {
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
}
const {
credentials: { apiKey, secretKey }
} = appConnection;
try {
const allZones: TDNSMadeEasyZone[] = [];
let currentPage = 0;
let totalPages = 1;
// Fetch all pages of zones
while (currentPage < totalPages) {
// eslint-disable-next-line no-await-in-loop
const resp = await request.get<DNSMadeEasyApiResponse>(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
headers: {
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
Accept: "application/json"
},
params: {
page: currentPage
}
});
if (resp.data?.data) {
// Map the API response to TDNSMadeEasyZone format
const zones = resp.data.data.map((zone) => ({
id: String(zone.id),
name: zone.name
}));
allZones.push(...zones);
// Update pagination info
totalPages = resp.data.totalPages || 1;
currentPage += 1;
} else {
break;
}
}
return allZones;
} catch (error: unknown) {
logger.error(error, "Error listing DNS Made Easy zones");
if (error instanceof AxiosError) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message: `Failed to list DNS Made Easy zones: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to list DNS Made Easy zones"
});
}
};
export const listDNSMadeEasyRecords = async (
appConnection: TDNSMadeEasyConnection,
options: { zoneId: string; type?: string; name?: string }
): Promise<DNSMadeEasyApiResponse["data"]> => {
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
}
const {
credentials: { apiKey, secretKey }
} = appConnection;
const { zoneId, type, name } = options;
try {
const allRecords: DNSMadeEasyApiResponse["data"] = [];
let currentPage = 0;
let totalPages = 1;
// Fetch all pages of records
while (currentPage < totalPages) {
// Build query parameters
const queryParams: Record<string, string | number> = {};
if (type) {
queryParams.type = type;
}
if (name) {
queryParams.recordName = name;
}
queryParams.page = currentPage;
// eslint-disable-next-line no-await-in-loop
const resp = await request.get<DNSMadeEasyApiResponse>(
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(zoneId)}/records`),
{
headers: {
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
Accept: "application/json"
},
params: queryParams
}
);
if (resp.data?.data) {
allRecords.push(...resp.data.data);
// Update pagination info
totalPages = resp.data.totalPages || 1;
currentPage += 1;
} else {
break;
}
}
return allRecords;
} catch (error: unknown) {
logger.error(error, "Error listing DNS Made Easy records");
if (error instanceof AxiosError) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message: `Failed to list DNS Made Easy records: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to list DNS Made Easy records"
});
}
};
export const validateDNSMadeEasyConnectionCredentials = async (config: TDNSMadeEasyConnectionConfig) => {
if (config.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
}
const { apiKey, secretKey } = config.credentials;
try {
const resp = await request.get(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
headers: {
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
Accept: "application/json"
}
});
if (resp.status !== 200) {
throw new BadRequestError({
message: "Unable to validate connection: Invalid API credentials provided."
});
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message: `Failed to validate credentials: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
});
}
logger.error(error, "Error validating DNS Made Easy connection credentials");
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
return config.credentials;
};
@@ -0,0 +1,64 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
export const DNSMadeEasyConnectionApiKeyCredentialsSchema = z.object({
apiKey: z.string().trim().min(1, "API key required").max(256, "API key cannot exceed 256 characters"),
secretKey: z.string().trim().min(1, "Secret key required").max(256, "Secret key cannot exceed 256 characters")
});
const BaseDNSMadeEasyConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.DNSMadeEasy)
});
export const DNSMadeEasyConnectionSchema = BaseDNSMadeEasyConnectionSchema.extend({
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema
});
export const SanitizedDNSMadeEasyConnectionSchema = z.discriminatedUnion("method", [
BaseDNSMadeEasyConnectionSchema.extend({
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.pick({ apiKey: true })
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy]} (API Key)` }))
]);
export const ValidateDNSMadeEasyConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(DNSMadeEasyConnectionMethod.APIKeySecret)
.describe(AppConnections.CREATE(AppConnection.DNSMadeEasy).method),
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.DNSMadeEasy).credentials
)
})
]);
export const CreateDNSMadeEasyConnectionSchema = ValidateDNSMadeEasyConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy)
);
export const UpdateDNSMadeEasyConnectionSchema = z
.object({
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.DNSMadeEasy).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy));
export const DNSMadeEasyConnectionListItemSchema = z
.object({
name: z.literal("DNS Made Easy"),
app: z.literal(AppConnection.DNSMadeEasy),
methods: z.nativeEnum(DNSMadeEasyConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy] }));
@@ -0,0 +1,35 @@
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listDNSMadeEasyZones } from "./dns-made-easy-connection-fns";
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TDNSMadeEasyConnection>;
export const dnsMadeEasyConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listZones = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.DNSMadeEasy, connectionId, actor);
try {
const zones = await listDNSMadeEasyZones(appConnection);
return zones;
} catch (error) {
logger.error(
error,
`Failed to list DNS Made Easy zones for DNS Made Easy connection [connectionId=${connectionId}]`
);
throw new BadRequestError({
message: `Failed to list DNS Made Easy zones: ${error instanceof Error ? error.message : "Unknown error"}`
});
}
};
return {
listZones
};
};
@@ -0,0 +1,30 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateDNSMadeEasyConnectionSchema,
DNSMadeEasyConnectionSchema,
ValidateDNSMadeEasyConnectionCredentialsSchema
} from "./dns-made-easy-connection-schema";
export type TDNSMadeEasyConnection = z.infer<typeof DNSMadeEasyConnectionSchema>;
export type TDNSMadeEasyConnectionInput = z.infer<typeof CreateDNSMadeEasyConnectionSchema> & {
app: AppConnection.DNSMadeEasy;
};
export type TValidateDNSMadeEasyConnectionCredentialsSchema = typeof ValidateDNSMadeEasyConnectionCredentialsSchema;
export type TDNSMadeEasyConnectionConfig = DiscriminativePick<
TDNSMadeEasyConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TDNSMadeEasyZone = {
id: string;
name: string;
};
@@ -663,7 +663,8 @@ export const authLoginServiceFactory = ({
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
ip: ipAddress, ip: ipAddress,
userAgent, userAgent,
siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com") siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com"),
orgId: organizationId
}, },
template: SmtpTemplates.OrgAdminBreakglassAccess template: SmtpTemplates.OrgAdminBreakglassAccess
}); });
@@ -1,4 +1,5 @@
export enum AcmeDnsProvider { export enum AcmeDnsProvider {
Route53 = "route53", Route53 = "route53",
Cloudflare = "cloudflare" Cloudflare = "cloudflare",
DNSMadeEasy = "dns-made-easy"
} }

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