Merge branch 'main' of https://github.com/Infisical/infisical into feat/suborg-scope-support

This commit is contained in:
Piyush Gupta
2025-12-01 15:22:45 +05:30
212 changed files with 4386 additions and 1472 deletions
+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}"`);
}
+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
@@ -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
@@ -24,7 +24,7 @@ Feature: Challenge
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,7 +58,7 @@ 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
@@ -153,7 +153,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 +165,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": {
@@ -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
} }
@@ -88,7 +88,7 @@ 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
@@ -271,7 +271,7 @@ 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
@@ -2,7 +2,7 @@ Feature: Internal CA
Scenario: CSR with SANs only Scenario: CSR with SANs only
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
+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": {
+8 -8
View File
@@ -56,7 +56,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 +74,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()
@@ -187,7 +187,7 @@ def step_impl(context: Context, var_name: str):
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"],
@@ -208,7 +208,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"],
@@ -228,7 +228,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"],
@@ -246,7 +246,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()
@@ -270,7 +270,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"],
@@ -288,7 +288,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()
+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",
@@ -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 => {
@@ -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
}, },
+9 -10
View File
@@ -390,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(
@@ -426,7 +424,8 @@ export const scimServiceFactory = ({
aliasType, aliasType,
externalId, externalId,
emails: email ? [email.toLowerCase()] : [], emails: email ? [email.toLowerCase()] : [],
orgId orgId,
isEmailVerified: trustScimEmails
}, },
tx tx
); );
+4
View File
@@ -1976,9 +1976,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.",
@@ -1986,9 +1988,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.",
@@ -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) => {
@@ -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);
}
});
}; };
+517 -261
View File
@@ -4,24 +4,510 @@ import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas"; import { CertificatesSchema } 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_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs"; import { ApiDocsTags, CERTIFICATES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addNoCacheHeaders } from "@app/server/lib/caching"; import { addNoCacheHeaders } from "@app/server/lib/caching";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
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, CrlReason } from "@app/services/certificate/certificate-types";
import { import {
validateAltNamesField, ACMESANType,
validateCaDateField CertificateOrderStatus,
} from "@app/services/certificate-authority/certificate-authority-validators"; CertKeyAlgorithm,
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; CertSignatureAlgorithm,
CrlReason
} from "@app/services/certificate/certificate-types";
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSubjectAlternativeNameType
} from "@app/services/certificate-common/certificate-constants";
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
import { booleanSchema } from "../sanitizedSchemas";
interface CertificateRequestForService {
commonName?: string;
keyUsages?: CertKeyUsageType[];
extendedKeyUsages?: CertExtendedKeyUsageType[];
altNames?: Array<{
type: CertSubjectAlternativeNameType;
value: string;
}>;
validity: {
ttl: string;
};
notBefore?: Date;
notAfter?: Date;
signatureAlgorithm?: string;
keyAlgorithm?: string;
}
const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => {
const hasDateFields = data.notBefore || data.notAfter;
const hasTtl = data.ttl;
return !(hasDateFields && hasTtl);
};
const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
if (data.notBefore && data.notAfter) {
const notBefore = new Date(data.notBefore);
const notAfter = new Date(data.notAfter);
return notBefore < notAfter;
}
return true;
};
export const registerCertificateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/issue-certificate",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
body: z
.object({
profileId: z.string().uuid(),
commonName: validateTemplateRegexField.optional(),
ttl: z
.string()
.trim()
.min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(),
altNames: z
.array(
z.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
value: z.string().min(1, "SAN value cannot be empty")
})
)
.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
removeRootsFromChain: booleanSchema.default(false).optional()
})
.refine(validateTtlAndDateFields, {
message:
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
})
.refine(validateDateOrder, {
message: "notBefore must be earlier than notAfter"
}),
response: {
200: z.object({
certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(),
privateKey: z.string().trim().optional(),
serialNumber: z.string().trim(),
certificateId: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateRequestForService: CertificateRequestForService = {
commonName: req.body.commonName,
keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages,
altNames: req.body.altNames,
validity: {
ttl: req.body.ttl
},
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
signatureAlgorithm: req.body.signatureAlgorithm,
keyAlgorithm: req.body.keyAlgorithm
};
const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService);
const data = await server.services.certificateV3.issueCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.body.profileId,
certificateRequest: mappedCertificateRequest,
removeRootsFromChain: req.body.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: req.body.profileId,
certificateId: data.certificateId,
commonName: req.body.commonName || "",
profileName: data.profileName
}
}
});
return data;
}
});
server.route({
method: "POST",
url: "/sign-certificate",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
body: z
.object({
profileId: z.string().uuid(),
csr: z.string().trim().min(1, "CSR cannot be empty").max(4096, "CSR cannot exceed 4096 characters"),
ttl: z
.string()
.trim()
.min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(),
removeRootsFromChain: booleanSchema.default(false).optional()
})
.refine(validateTtlAndDateFields, {
message:
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
})
.refine(validateDateOrder, {
message: "notBefore must be earlier than notAfter"
}),
response: {
200: z.object({
certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(),
serialNumber: z.string().trim(),
certificateId: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateRequest = extractCertificateRequestFromCSR(req.body.csr);
const data = await server.services.certificateV3.signCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.body.profileId,
csr: req.body.csr,
validity: {
ttl: req.body.ttl
},
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
enrollmentType: EnrollmentType.API,
removeRootsFromChain: req.body.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: req.body.profileId,
certificateId: data.certificateId,
profileName: data.profileName,
commonName: certificateRequest.commonName || ""
}
}
});
return data;
}
});
server.route({
method: "POST",
url: "/order-certificate",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
body: z
.object({
profileId: z.string().uuid(),
subjectAlternativeNames: z
.array(
z.object({
type: z.nativeEnum(ACMESANType),
value: z
.string()
.trim()
.min(1, "SAN value cannot be empty")
.max(255, "SAN value must be less than 255 characters")
})
)
.min(1, "At least one subject alternative name must be provided"),
ttl: z
.string()
.trim()
.min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(),
commonName: validateTemplateRegexField.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
removeRootsFromChain: booleanSchema.default(false).optional()
})
.refine(validateTtlAndDateFields, {
message:
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
})
.refine(validateDateOrder, {
message: "notBefore must be earlier than notAfter"
}),
response: {
200: z.object({
orderId: z.string(),
status: z.nativeEnum(CertificateOrderStatus),
subjectAlternativeNames: z.array(
z.object({
type: z.nativeEnum(ACMESANType),
value: z.string(),
status: z.nativeEnum(CertificateOrderStatus)
})
),
authorizations: z.array(
z.object({
identifier: z.object({
type: z.nativeEnum(ACMESANType),
value: z.string()
}),
status: z.nativeEnum(CertificateOrderStatus),
expires: z.string().optional(),
challenges: z.array(
z.object({
type: z.string(),
status: z.nativeEnum(CertificateOrderStatus),
url: z.string(),
token: z.string()
})
)
})
),
finalize: z.string(),
certificate: z.string().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.certificateV3.orderCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.body.profileId,
certificateOrder: {
altNames: req.body.subjectAlternativeNames,
validity: {
ttl: req.body.ttl
},
commonName: req.body.commonName,
keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages,
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
signatureAlgorithm: req.body.signatureAlgorithm,
keyAlgorithm: req.body.keyAlgorithm
},
removeRootsFromChain: req.body.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: req.body.profileId,
orderId: data.orderId,
profileName: data.profileName
}
}
});
return data;
}
});
server.route({
method: "POST",
url: "/:id/renew",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
params: z.object({
id: z.string().uuid()
}),
body: z
.object({
removeRootsFromChain: booleanSchema.default(false).optional()
})
.optional(),
response: {
200: z.object({
certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(),
privateKey: z.string().trim().optional(),
serialNumber: z.string().trim(),
certificateId: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.certificateV3.renewCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.id,
removeRootsFromChain: req.body?.removeRootsFromChain
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.RENEW_CERTIFICATE,
metadata: {
originalCertificateId: req.params.id,
newCertificateId: data.certificateId,
profileName: data.profileName,
commonName: data.commonName
}
}
});
return data;
}
});
server.route({
method: "PATCH",
url: "/:id/config",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
params: z.object({
id: z.string().uuid()
}),
body: z
.object({
renewBeforeDays: z.number().int().min(1).max(30).optional(),
enableAutoRenewal: z.boolean().optional()
})
.refine((data) => !(data.renewBeforeDays !== undefined && data.enableAutoRenewal === false), {
message: "Cannot specify both renewBeforeDays and enableAutoRenewal=false"
}),
response: {
200: z.object({
message: z.string(),
renewBeforeDays: z.number().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
if (req.body.enableAutoRenewal === false) {
const data = await server.services.certificateV3.disableRenewalConfig({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG,
metadata: {
certificateId: req.params.id,
commonName: data.commonName
}
}
});
return {
message: "Auto-renewal disabled successfully"
};
}
if (req.body.renewBeforeDays !== undefined) {
const data = await server.services.certificateV3.updateRenewalConfig({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
certificateId: req.params.id,
renewBeforeDays: req.body.renewBeforeDays
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG,
metadata: {
certificateId: req.params.id,
renewBeforeDays: req.body.renewBeforeDays.toString(),
commonName: data.commonName
}
}
});
return {
message: "Certificate configuration updated successfully",
renewBeforeDays: data.renewBeforeDays
};
}
return {
message: "No configuration changes requested"
};
}
});
export const registerCertRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:serialNumber", url: "/:id",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -31,7 +517,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
description: "Get certificate", description: "Get certificate",
params: z.object({ params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) id: z.string().trim().describe(CERTIFICATES.GET.id)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -41,7 +527,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const { cert } = await server.services.certificate.getCert({ const { cert } = await server.services.certificate.getCert({
serialNumber: req.params.serialNumber, id: req.params.id,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -67,10 +553,9 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
} }
}); });
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
server.route({ server.route({
method: "GET", method: "GET",
url: "/:serialNumber/private-key", url: "/:id/private-key",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -80,7 +565,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
description: "Get certificate private key", description: "Get certificate private key",
params: z.object({ params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) id: z.string().trim().describe(CERTIFICATES.GET.id)
}), }),
response: { response: {
200: z.string().trim() 200: z.string().trim()
@@ -88,7 +573,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req, reply) => { handler: async (req, reply) => {
const { cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({ const { cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
serialNumber: req.params.serialNumber, id: req.params.id,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -114,10 +599,9 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
} }
}); });
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
server.route({ server.route({
method: "GET", method: "GET",
url: "/:serialNumber/bundle", url: "/:id/bundle",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -127,7 +611,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
description: "Get certificate bundle including the certificate, chain, and private key.", description: "Get certificate bundle including the certificate, chain, and private key.",
params: z.object({ params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) id: z.string().trim().describe(CERTIFICATES.GET_CERT.id)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -141,7 +625,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
handler: async (req, reply) => { handler: async (req, reply) => {
const { certificate, certificateChain, serialNumber, cert, privateKey } = const { certificate, certificateChain, serialNumber, cert, privateKey } =
await server.services.certificate.getCertBundle({ await server.services.certificate.getCertBundle({
serialNumber: req.params.serialNumber, id: req.params.id,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -172,120 +656,6 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "POST",
url: "/issue-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
description: "Issue certificate",
body: z
.object({
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId),
certificateTemplateId: z
.string()
.trim()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter),
keyUsages: z
.nativeEnum(CertKeyUsage)
.array()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.keyUsages),
extendedKeyUsages: z
.nativeEnum(CertExtendedKeyUsage)
.array()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.extendedKeyUsages)
})
.refine(
(data) => {
const { ttl, notAfter } = data;
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
},
{
message: "Either ttl or notAfter must be present, but not both",
path: ["ttl", "notAfter"]
}
)
.refine(
(data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
(data.caId === undefined && data.certificateTemplateId !== undefined),
{
message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"]
}
),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
await server.services.internalCertificateAuthority.issueCertFromCa({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.ISSUE_CERT,
metadata: {
caId: ca.id,
dn: ca.dn,
serialNumber
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.IssueCert,
distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId,
properties: {
caId: req.body.caId,
certificateTemplateId: req.body.certificateTemplateId,
commonName: req.body.commonName,
...req.auditLogInfo
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
privateKey,
serialNumber
};
}
});
server.route({ server.route({
method: "POST", method: "POST",
url: "/import-certificate", url: "/import-certificate",
@@ -350,121 +720,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/sign-certificate", url: "/:id/revoke",
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: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -474,7 +730,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
description: "Revoke", description: "Revoke",
params: z.object({ params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumber) id: z.string().trim().describe(CERTIFICATES.REVOKE.id)
}), }),
body: z.object({ body: z.object({
revocationReason: z.nativeEnum(CrlReason).describe(CERTIFICATES.REVOKE.revocationReason) revocationReason: z.nativeEnum(CrlReason).describe(CERTIFICATES.REVOKE.revocationReason)
@@ -489,7 +745,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const { revokedAt, cert, ca } = await server.services.certificate.revokeCert({ const { revokedAt, cert, ca } = await server.services.certificate.revokeCert({
serialNumber: req.params.serialNumber, id: req.params.id,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -512,7 +768,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
return { return {
message: "Successfully revoked certificate", message: "Successfully revoked certificate",
serialNumber: req.params.serialNumber, serialNumber: cert.serialNumber,
revokedAt revokedAt
}; };
} }
@@ -520,7 +776,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:serialNumber", url: "/:id",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -530,7 +786,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
description: "Delete certificate", description: "Delete certificate",
params: z.object({ params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.DELETE.serialNumber) id: z.string().trim().describe(CERTIFICATES.DELETE.id)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -540,7 +796,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const { deletedCert } = await server.services.certificate.deleteCert({ const { deletedCert } = await server.services.certificate.deleteCert({
serialNumber: req.params.serialNumber, id: req.params.id,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -568,7 +824,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:serialNumber/certificate", url: "/:id/certificate",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
@@ -578,7 +834,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
description: "Get certificate body of certificate", description: "Get certificate body of certificate",
params: z.object({ params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber) id: z.string().trim().describe(CERTIFICATES.GET_CERT.id)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -590,7 +846,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req) => { handler: async (req) => {
const { certificate, certificateChain, serialNumber, cert } = await server.services.certificate.getCertBody({ const { certificate, certificateChain, serialNumber, cert } = await server.services.certificate.getCertBody({
serialNumber: req.params.serialNumber, id: req.params.id,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -620,7 +876,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/:serialNumber/pkcs12", url: "/:id/pkcs12",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -630,7 +886,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
description: "Download certificate in PKCS12 format", description: "Download certificate in PKCS12 format",
params: z.object({ params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) id: z.string().trim().describe(CERTIFICATES.GET.id)
}), }),
body: z.object({ body: z.object({
password: z password: z
@@ -645,7 +901,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req, reply) => { handler: async (req, reply) => {
const { pkcs12Data, cert } = await server.services.certificate.getCertPkcs12({ const { pkcs12Data, cert } = await server.services.certificate.getCertPkcs12({
serialNumber: req.params.serialNumber, id: req.params.id,
password: req.body.password, password: req.body.password,
alias: req.body.alias, alias: req.body.alias,
actor: req.permission.type, actor: req.permission.type,
@@ -671,7 +927,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
reply.header("Content-Type", "application/octet-stream"); reply.header("Content-Type", "application/octet-stream");
reply.header( reply.header(
"Content-Disposition", "Content-Disposition",
`attachment; filename="certificate-${req.params.serialNumber.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"` `attachment; filename="certificate-${cert.serialNumber?.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"`
); );
return pkcs12Data; return pkcs12Data;
@@ -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;
}
});
};
+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(
+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";
+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";
@@ -419,7 +419,6 @@ export const AcmeCertificateAuthorityFns = ({
name, name,
projectId, projectId,
configuration, configuration,
enableDirectIssuance,
actor, actor,
status status
}: { }: {
@@ -427,7 +426,6 @@ export const AcmeCertificateAuthorityFns = ({
name: string; name: string;
projectId: string; projectId: string;
configuration: TCreateAcmeCertificateAuthorityDTO["configuration"]; configuration: TCreateAcmeCertificateAuthorityDTO["configuration"];
enableDirectIssuance: boolean;
actor: OrgServiceActor; actor: OrgServiceActor;
}) => { }) => {
if (crypto.isFipsModeEnabled()) { if (crypto.isFipsModeEnabled()) {
@@ -473,7 +471,7 @@ export const AcmeCertificateAuthorityFns = ({
const ca = await certificateAuthorityDAL.create( const ca = await certificateAuthorityDAL.create(
{ {
projectId, projectId,
enableDirectIssuance, enableDirectIssuance: false,
name, name,
status status
}, },
@@ -521,14 +519,12 @@ export const AcmeCertificateAuthorityFns = ({
id, id,
status, status,
configuration, configuration,
enableDirectIssuance,
actor, actor,
name name
}: { }: {
id: string; id: string;
status?: CaStatus; status?: CaStatus;
configuration: TUpdateAcmeCertificateAuthorityDTO["configuration"]; configuration: TUpdateAcmeCertificateAuthorityDTO["configuration"];
enableDirectIssuance?: boolean;
actor: OrgServiceActor; actor: OrgServiceActor;
name?: string; name?: string;
}) => { }) => {
@@ -598,13 +594,12 @@ export const AcmeCertificateAuthorityFns = ({
); );
} }
if (name || status || enableDirectIssuance) { if (name || status) {
await certificateAuthorityDAL.updateById( await certificateAuthorityDAL.updateById(
id, id,
{ {
name, name,
status, status
enableDirectIssuance
}, },
tx tx
); );
@@ -0,0 +1,14 @@
import { CaType } from "../certificate-authority-enums";
import {
GenericCreateCertificateAuthorityFieldsSchema,
GenericUpdateCertificateAuthorityFieldsSchema
} from "../deprecated-certificate-authority-schemas";
import { AcmeCertificateAuthorityConfigurationSchema } from "./acme-certificate-authority-schemas";
export const CreateAcmeCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(CaType.ACME).extend({
configuration: AcmeCertificateAuthorityConfigurationSchema
});
export const UpdateAcmeCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(CaType.ACME).extend({
configuration: AcmeCertificateAuthorityConfigurationSchema.optional()
});
@@ -597,7 +597,6 @@ export const AzureAdCsCertificateAuthorityFns = ({
name, name,
projectId, projectId,
configuration, configuration,
enableDirectIssuance,
actor, actor,
status status
}: { }: {
@@ -605,16 +604,8 @@ export const AzureAdCsCertificateAuthorityFns = ({
name: string; name: string;
projectId: string; projectId: string;
configuration: TCreateAzureAdCsCertificateAuthorityDTO["configuration"]; configuration: TCreateAzureAdCsCertificateAuthorityDTO["configuration"];
enableDirectIssuance: boolean;
actor: OrgServiceActor; actor: OrgServiceActor;
}) => { }) => {
// Azure ADCS does not support direct issuance - enforce this restriction
if (enableDirectIssuance) {
throw new BadRequestError({
message: "Azure ADCS Certificate Authorities do not support direct issuance"
});
}
const { azureAdcsConnectionId } = configuration; const { azureAdcsConnectionId } = configuration;
const appConnection = await appConnectionDAL.findById(azureAdcsConnectionId); const appConnection = await appConnectionDAL.findById(azureAdcsConnectionId);
@@ -679,24 +670,15 @@ export const AzureAdCsCertificateAuthorityFns = ({
id, id,
status, status,
configuration, configuration,
enableDirectIssuance,
actor, actor,
name name
}: { }: {
id: string; id: string;
status?: CaStatus; status?: CaStatus;
configuration: TUpdateAzureAdCsCertificateAuthorityDTO["configuration"]; configuration: TUpdateAzureAdCsCertificateAuthorityDTO["configuration"];
enableDirectIssuance?: boolean;
actor: OrgServiceActor; actor: OrgServiceActor;
name?: string; name?: string;
}) => { }) => {
// Azure ADCS does not support direct issuance - enforce this restriction
if (enableDirectIssuance) {
throw new BadRequestError({
message: "Azure ADCS Certificate Authorities do not support direct issuance"
});
}
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
if (configuration) { if (configuration) {
const { azureAdcsConnectionId } = configuration; const { azureAdcsConnectionId } = configuration;
@@ -737,13 +719,12 @@ export const AzureAdCsCertificateAuthorityFns = ({
); );
} }
if (name || status || enableDirectIssuance !== undefined) { if (name || status) {
await certificateAuthorityDAL.updateById( await certificateAuthorityDAL.updateById(
id, id,
{ {
name, name,
status, status
enableDirectIssuance: false // Always false for Azure ADCS CAs
}, },
tx tx
); );
@@ -0,0 +1,18 @@
import { CaType } from "../certificate-authority-enums";
import {
GenericCreateCertificateAuthorityFieldsSchema,
GenericUpdateCertificateAuthorityFieldsSchema
} from "../deprecated-certificate-authority-schemas";
import { AzureAdCsCertificateAuthorityConfigurationSchema } from "./azure-ad-cs-certificate-authority-schemas";
export const CreateAzureAdCsCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(
CaType.AZURE_AD_CS
).extend({
configuration: AzureAdCsCertificateAuthorityConfigurationSchema
});
export const UpdateAzureAdCsCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(
CaType.AZURE_AD_CS
).extend({
configuration: AzureAdCsCertificateAuthorityConfigurationSchema.optional()
});
@@ -19,14 +19,10 @@ export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({ z.object({
name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name), name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name),
projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.CREATE(type).projectId), projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.CREATE(type).projectId),
enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status) status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status)
}); });
export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) => export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({ z.object({
name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name),
projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.UPDATE(type).projectId),
enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status) status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status)
}); });
@@ -37,6 +37,7 @@ import { CaType } from "./certificate-authority-enums";
import { import {
TCertificateAuthority, TCertificateAuthority,
TCreateCertificateAuthorityDTO, TCreateCertificateAuthorityDTO,
TDeprecatedUpdateCertificateAuthorityDTO,
TUpdateCertificateAuthorityDTO TUpdateCertificateAuthorityDTO
} from "./certificate-authority-types"; } from "./certificate-authority-types";
import { TExternalCertificateAuthorityDALFactory } from "./external-certificate-authority-dal"; import { TExternalCertificateAuthorityDALFactory } from "./external-certificate-authority-dal";
@@ -123,7 +124,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
const createCertificateAuthority = async ( const createCertificateAuthority = async (
{ type, projectId, name, enableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO, { type, projectId, name, configuration, status }: TCreateCertificateAuthorityDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
@@ -145,7 +146,6 @@ export const certificateAuthorityServiceFactory = ({
...(configuration as TCreateInternalCertificateAuthorityDTO["configuration"]), ...(configuration as TCreateInternalCertificateAuthorityDTO["configuration"]),
isInternal: true, isInternal: true,
projectId, projectId,
enableDirectIssuance,
name name
}); });
@@ -171,7 +171,6 @@ export const certificateAuthorityServiceFactory = ({
name, name,
projectId, projectId,
configuration: configuration as TCreateAcmeCertificateAuthorityDTO["configuration"], configuration: configuration as TCreateAcmeCertificateAuthorityDTO["configuration"],
enableDirectIssuance,
status, status,
actor actor
}); });
@@ -182,7 +181,6 @@ export const certificateAuthorityServiceFactory = ({
name, name,
projectId, projectId,
configuration: configuration as TCreateAzureAdCsCertificateAuthorityDTO["configuration"], configuration: configuration as TCreateAzureAdCsCertificateAuthorityDTO["configuration"],
enableDirectIssuance,
status, status,
actor actor
}); });
@@ -191,6 +189,63 @@ export const certificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "Invalid certificate authority type" }); throw new BadRequestError({ message: "Invalid certificate authority type" });
}; };
const findCertificateAuthorityById = async ({ id, type }: { id: string; type: CaType }, actor: OrgServiceActor) => {
const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id);
if (!certificateAuthority)
throw new NotFoundError({
message: `Could not find certificate authority with id "${id}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId: certificateAuthority.projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.CertificateAuthorities
);
if (type === CaType.INTERNAL) {
if (!certificateAuthority.internalCa?.id) {
throw new NotFoundError({
message: `Internal certificate authority with id "${id}" not found`
});
}
return {
id: certificateAuthority.id,
type,
enableDirectIssuance: certificateAuthority.enableDirectIssuance,
name: certificateAuthority.name,
projectId: certificateAuthority.projectId,
configuration: certificateAuthority.internalCa,
status: certificateAuthority.status
} as TCertificateAuthority;
}
if (certificateAuthority.externalCa?.type !== type) {
throw new NotFoundError({
message: `Could not find external certificate authority with id ${id} and type "${type}"`
});
}
if (type === CaType.ACME) {
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
}
if (type === CaType.AZURE_AD_CS) {
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
}
throw new BadRequestError({ message: "Invalid certificate authority type" });
};
const findCertificateAuthorityByNameAndProjectId = async ( const findCertificateAuthorityByNameAndProjectId = async (
{ caName, type, projectId }: { caName: string; type: CaType; projectId: string }, { caName, type, projectId }: { caName: string; type: CaType; projectId: string },
actor: OrgServiceActor actor: OrgServiceActor
@@ -303,7 +358,145 @@ export const certificateAuthorityServiceFactory = ({
}; };
const updateCertificateAuthority = async ( const updateCertificateAuthority = async (
{ caName, type, configuration, enableDirectIssuance, status, name, projectId }: TUpdateCertificateAuthorityDTO, { id, type, configuration, status, name }: TUpdateCertificateAuthorityDTO,
actor: OrgServiceActor
) => {
const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id);
if (!certificateAuthority)
throw new NotFoundError({
message: `Could not find certificate authority with id "${id}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId: certificateAuthority.projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateAuthorities
);
if (type === CaType.INTERNAL) {
if (!certificateAuthority.internalCa?.id) {
throw new NotFoundError({
message: `Internal certificate authority with id "${id}" not found`
});
}
const updatedCa = await internalCertificateAuthorityService.updateCaById({
isInternal: true,
caId: certificateAuthority.id,
status,
name
});
if (!updatedCa.internalCa) {
throw new BadRequestError({
message: "Failed to update internal certificate authority"
});
}
return {
id: updatedCa.id,
type,
enableDirectIssuance: updatedCa.enableDirectIssuance,
name: updatedCa.name,
projectId: updatedCa.projectId,
configuration: updatedCa.internalCa,
status: updatedCa.status
} as TCertificateAuthority;
}
if (type === CaType.ACME) {
return acmeFns.updateCertificateAuthority({
id: certificateAuthority.id,
configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"],
actor,
status,
name
});
}
if (type === CaType.AZURE_AD_CS) {
return azureAdCsFns.updateCertificateAuthority({
id: certificateAuthority.id,
configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"],
actor,
status,
name
});
}
throw new BadRequestError({ message: "Invalid certificate authority type" });
};
const deleteCertificateAuthority = async ({ id, type }: { id: string; type: CaType }, actor: OrgServiceActor) => {
const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id);
if (!certificateAuthority)
throw new NotFoundError({
message: `Could not find certificate authority with id "${id}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId: certificateAuthority.projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.CertificateAuthorities
);
if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) {
throw new BadRequestError({
message: "Internal certificate authority cannot be deleted"
});
}
if (certificateAuthority.externalCa?.id && certificateAuthority.externalCa.type !== type) {
throw new BadRequestError({
message: "External certificate authority cannot be deleted"
});
}
await certificateAuthorityDAL.deleteById(certificateAuthority.id);
if (type === CaType.INTERNAL) {
return {
id: certificateAuthority.id,
type,
enableDirectIssuance: certificateAuthority.enableDirectIssuance,
name: certificateAuthority.name,
projectId: certificateAuthority.projectId,
configuration: certificateAuthority.internalCa,
status: certificateAuthority.status
} as TCertificateAuthority;
}
if (type === CaType.ACME) {
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
}
if (type === CaType.AZURE_AD_CS) {
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
}
throw new BadRequestError({ message: "Invalid certificate authority type" });
};
const deprecatedUpdateCertificateAuthority = async (
{ caName, type, configuration, status, name, projectId }: TDeprecatedUpdateCertificateAuthorityDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const certificateAuthority = await certificateAuthorityDAL.findByNameAndProjectIdWithAssociatedCa( const certificateAuthority = await certificateAuthorityDAL.findByNameAndProjectIdWithAssociatedCa(
@@ -339,7 +532,6 @@ export const certificateAuthorityServiceFactory = ({
const updatedCa = await internalCertificateAuthorityService.updateCaById({ const updatedCa = await internalCertificateAuthorityService.updateCaById({
isInternal: true, isInternal: true,
enableDirectIssuance,
caId: certificateAuthority.id, caId: certificateAuthority.id,
status, status,
name name
@@ -366,7 +558,6 @@ export const certificateAuthorityServiceFactory = ({
return acmeFns.updateCertificateAuthority({ return acmeFns.updateCertificateAuthority({
id: certificateAuthority.id, id: certificateAuthority.id,
configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"], configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"],
enableDirectIssuance,
actor, actor,
status, status,
name name
@@ -377,7 +568,6 @@ export const certificateAuthorityServiceFactory = ({
return azureAdCsFns.updateCertificateAuthority({ return azureAdCsFns.updateCertificateAuthority({
id: certificateAuthority.id, id: certificateAuthority.id,
configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"], configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"],
enableDirectIssuance,
actor, actor,
status, status,
name name
@@ -387,7 +577,7 @@ export const certificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "Invalid certificate authority type" }); throw new BadRequestError({ message: "Invalid certificate authority type" });
}; };
const deleteCertificateAuthority = async ( const deprecatedDeleteCertificateAuthority = async (
{ caName, type, projectId }: { caName: string; type: CaType; projectId: string }, { caName, type, projectId }: { caName: string; type: CaType; projectId: string },
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
@@ -489,10 +679,13 @@ export const certificateAuthorityServiceFactory = ({
return { return {
createCertificateAuthority, createCertificateAuthority,
findCertificateAuthorityByNameAndProjectId, findCertificateAuthorityById,
listCertificateAuthoritiesByProjectId, listCertificateAuthoritiesByProjectId,
findCertificateAuthorityByNameAndProjectId,
updateCertificateAuthority, updateCertificateAuthority,
deleteCertificateAuthority, deleteCertificateAuthority,
getAzureAdcsTemplates getAzureAdcsTemplates,
deprecatedUpdateCertificateAuthority,
deprecatedDeleteCertificateAuthority
}; };
}; };
@@ -19,9 +19,14 @@ export type TCertificateAuthorityInput =
| TAcmeCertificateAuthorityInput | TAcmeCertificateAuthorityInput
| TCreateAzureAdCsCertificateAuthorityDTO; | TCreateAzureAdCsCertificateAuthorityDTO;
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id">; export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id" | "enableDirectIssuance">;
export type TUpdateCertificateAuthorityDTO = Partial<Omit<TCreateCertificateAuthorityDTO, "projectId">> & { export type TUpdateCertificateAuthorityDTO = Partial<Omit<TCreateCertificateAuthorityDTO, "projectId">> & {
type: CaType;
id: string;
};
export type TDeprecatedUpdateCertificateAuthorityDTO = Partial<Omit<TCreateCertificateAuthorityDTO, "projectId">> & {
type: CaType; type: CaType;
caName: string; caName: string;
projectId: string; projectId: string;
@@ -0,0 +1,32 @@
import z from "zod";
import { CertificateAuthoritiesSchema } from "@app/db/schemas";
import { CertificateAuthorities } from "@app/lib/api-docs/constants";
import { slugSchema } from "@app/server/lib/schemas";
import { CaStatus, CaType } from "./certificate-authority-enums";
export const BaseCertificateAuthoritySchema = CertificateAuthoritiesSchema.pick({
projectId: true,
enableDirectIssuance: true,
name: true,
id: true
}).extend({
status: z.nativeEnum(CaStatus)
});
export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({
name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name),
projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.CREATE(type).projectId),
enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status)
});
export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({
name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name),
projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.UPDATE(type).projectId),
enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status)
});
@@ -0,0 +1,14 @@
import { CaType } from "../certificate-authority-enums";
import {
GenericCreateCertificateAuthorityFieldsSchema,
GenericUpdateCertificateAuthorityFieldsSchema
} from "../deprecated-certificate-authority-schemas";
import { InternalCertificateAuthorityConfigurationSchema } from "./internal-certificate-authority-schemas";
export const CreateInternalCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(
CaType.INTERNAL
).extend({
configuration: InternalCertificateAuthorityConfigurationSchema
});
export const UpdateInternalCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(CaType.INTERNAL);
@@ -136,8 +136,8 @@ export const InternalCertificateAuthorityFns = ({
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig(); const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
@@ -366,8 +366,8 @@ export const InternalCertificateAuthorityFns = ({
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig(); const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
@@ -11,7 +11,7 @@ import {
} from "../certificate-authority-schemas"; } from "../certificate-authority-schemas";
import { validateCaDateField } from "../certificate-authority-validators"; import { validateCaDateField } from "../certificate-authority-validators";
const InternalCertificateAuthorityConfigurationSchema = z export const InternalCertificateAuthorityConfigurationSchema = z
.object({ .object({
type: z.nativeEnum(InternalCaType).describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.type), type: z.nativeEnum(InternalCaType).describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.type),
friendlyName: z.string().optional().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.friendlyName), friendlyName: z.string().optional().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.friendlyName),
@@ -34,8 +34,6 @@ import {
CertExtendedKeyUsageOIDToName, CertExtendedKeyUsageOIDToName,
CertKeyAlgorithm, CertKeyAlgorithm,
CertKeyUsage, CertKeyUsage,
CertSignatureAlgorithm,
CertSignatureType,
CertStatus, CertStatus,
TAltNameMapping TAltNameMapping
} from "../../certificate/certificate-types"; } from "../../certificate/certificate-types";
@@ -127,6 +125,22 @@ export const internalCertificateAuthorityServiceFactory = ({
kmsService, kmsService,
permissionService permissionService
}: TInternalCertificateAuthorityServiceFactoryDep) => { }: TInternalCertificateAuthorityServiceFactoryDep) => {
const $checkSignature = (caKeyAlg: string, requestedKeyType: string, signatureAlgorithm?: string) => {
const isRsaCa = caKeyAlg.startsWith("RSA");
const isEcdsaCa = caKeyAlg.startsWith("EC") || caKeyAlg.startsWith("ECDSA");
// eslint-disable-next-line no-nested-ternary
const caSupports = isRsaCa ? "RSA" : isEcdsaCa ? "ECDSA" : "unknown";
const isRequestValid = (requestedKeyType === "RSA" && isRsaCa) || (requestedKeyType === "ECDSA" && isEcdsaCa);
if (!isRequestValid) {
throw new BadRequestError({
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlg}. CA can only sign with ${caSupports}-based signature algorithms.`
});
}
};
const createCa = async ({ const createCa = async ({
type, type,
friendlyName, friendlyName,
@@ -140,7 +154,6 @@ export const internalCertificateAuthorityServiceFactory = ({
notAfter, notAfter,
maxPathLength, maxPathLength,
keyAlgorithm, keyAlgorithm,
enableDirectIssuance,
name, name,
...dto ...dto
}: TCreateCaDTO) => { }: TCreateCaDTO) => {
@@ -192,9 +205,9 @@ export const internalCertificateAuthorityServiceFactory = ({
const ca = await certificateAuthorityDAL.create( const ca = await certificateAuthorityDAL.create(
{ {
projectId, projectId,
enableDirectIssuance,
name: name || slugify(`${(friendlyName || dn).slice(0, 16)}-${alphaNumericNanoId(8)}`), name: name || slugify(`${(friendlyName || dn).slice(0, 16)}-${alphaNumericNanoId(8)}`),
status: type === InternalCaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE status: type === InternalCaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE,
enableDirectIssuance: false
}, },
tx tx
); );
@@ -354,7 +367,7 @@ export const internalCertificateAuthorityServiceFactory = ({
* Update CA with id [caId]. * Update CA with id [caId].
* Note: Used to enable/disable CA * Note: Used to enable/disable CA
*/ */
const updateCaById = async ({ caId, status, enableDirectIssuance, name, ...dto }: TUpdateCaDTO) => { const updateCaById = async ({ caId, status, name, ...dto }: TUpdateCaDTO) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
@@ -375,8 +388,8 @@ export const internalCertificateAuthorityServiceFactory = ({
} }
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
if (enableDirectIssuance !== undefined || status !== undefined || name !== undefined) { if (status !== undefined || name !== undefined) {
await certificateAuthorityDAL.updateById(ca.id, { enableDirectIssuance, status, name }, tx); await certificateAuthorityDAL.updateById(ca.id, { status, name }, tx);
} }
return certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx); return certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx);
@@ -971,9 +984,9 @@ export const internalCertificateAuthorityServiceFactory = ({
const serialNumber = createSerialNumber(); const serialNumber = createSerialNumber();
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
const intermediateCert = await x509.X509CertificateGenerator.create({ const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,
@@ -1302,26 +1315,7 @@ export const internalCertificateAuthorityServiceFactory = ({
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]); const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]);
if (signatureAlgorithm) { if (signatureAlgorithm) {
const caKeyAlgorithm = ca.internalCa.keyAlgorithm; $checkSignature(ca.internalCa.keyAlgorithm, signatureAlgorithm.split("-")[0], signatureAlgorithm);
const requestedKeyType = signatureAlgorithm.split("-")[0];
const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]);
const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]);
if (
(requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) ||
(requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa)
) {
// eslint-disable-next-line no-nested-ternary
const supportedType = isRsaCa
? CertSignatureAlgorithm.RSA_SHA256.split("-")[0]
: isEcdsaCa
? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0]
: "unknown";
throw new BadRequestError({
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
});
}
} }
// Determine signing algorithm for certificate signing // Determine signing algorithm for certificate signing
@@ -1352,8 +1346,8 @@ export const internalCertificateAuthorityServiceFactory = ({
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig(); const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
@@ -1690,22 +1684,7 @@ export const internalCertificateAuthorityServiceFactory = ({
} }
if (signatureAlgorithm) { if (signatureAlgorithm) {
const caKeyAlgorithm = ca.internalCa.keyAlgorithm; $checkSignature(ca.internalCa.keyAlgorithm, signatureAlgorithm.split("-")[0], signatureAlgorithm);
const requestedKeyType = signatureAlgorithm.split("-")[0]; // Get the first part (RSA, ECDSA)
const isRsaCa = caKeyAlgorithm.startsWith(CertSignatureType.RSA);
const isEcdsaCa = caKeyAlgorithm.startsWith(CertSignatureType.ECDSA);
if (
(requestedKeyType === CertSignatureType.RSA && !isRsaCa) ||
(requestedKeyType === CertSignatureType.ECDSA && !isEcdsaCa)
) {
// eslint-disable-next-line no-nested-ternary
const supportedType = isRsaCa ? CertSignatureType.RSA : isEcdsaCa ? CertSignatureType.ECDSA : "unknown";
throw new BadRequestError({
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
});
}
} }
const effectiveKeyAlgorithm = (keyAlgorithm || ca.internalCa.keyAlgorithm) as CertKeyAlgorithm; const effectiveKeyAlgorithm = (keyAlgorithm || ca.internalCa.keyAlgorithm) as CertKeyAlgorithm;
@@ -1728,9 +1707,9 @@ export const internalCertificateAuthorityServiceFactory = ({
}); });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
@@ -48,7 +48,6 @@ export type TCreateCaDTO =
notAfter?: string; notAfter?: string;
maxPathLength?: number | null; maxPathLength?: number | null;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
enableDirectIssuance: boolean;
} }
| ({ | ({
isInternal: false; isInternal: false;
@@ -66,7 +65,6 @@ export type TCreateCaDTO =
notAfter?: string; notAfter?: string;
maxPathLength?: number | null; maxPathLength?: number | null;
keyAlgorithm: CertKeyAlgorithm; keyAlgorithm: CertKeyAlgorithm;
enableDirectIssuance: boolean;
} & Omit<TProjectPermission, "projectId">); } & Omit<TProjectPermission, "projectId">);
export type TGetCaDTO = { export type TGetCaDTO = {
@@ -79,14 +77,12 @@ export type TUpdateCaDTO =
caId: string; caId: string;
name?: string; name?: string;
status?: CaStatus; status?: CaStatus;
enableDirectIssuance?: boolean;
} }
| ({ | ({
isInternal: false; isInternal: false;
caId: string; caId: string;
name?: string; name?: string;
status?: CaStatus; status?: CaStatus;
enableDirectIssuance?: boolean;
} & Omit<TProjectPermission, "projectId">); } & Omit<TProjectPermission, "projectId">);
export type TDeleteCaDTO = { export type TDeleteCaDTO = {
@@ -1190,7 +1190,7 @@ export const certificateV3ServiceFactory = ({
status: CertificateOrderStatus.VALID status: CertificateOrderStatus.VALID
})), })),
authorizations: [], authorizations: [],
finalize: `/api/v3/pki/certificates/orders/${orderId}/completed`, finalize: `/api/v1/cert-manager/certificates/orders/${orderId}/completed`,
certificate: certificateResult.certificate, certificate: certificateResult.certificate,
projectId: certificateResult.projectId, projectId: certificateResult.projectId,
profileName: certificateResult.profileName profileName: certificateResult.profileName
@@ -52,7 +52,10 @@ import {
} from "./certificate-types"; } from "./certificate-types";
type TCertificateServiceFactoryDep = { type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find" | "transaction" | "create">; certificateDAL: Pick<
TCertificateDALFactory,
"findOne" | "deleteById" | "update" | "find" | "transaction" | "create" | "findById"
>;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">; certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
@@ -91,8 +94,8 @@ export const certificateServiceFactory = ({
/** /**
* Return details for certificate with serial number [serialNumber] * Return details for certificate with serial number [serialNumber]
*/ */
const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => { const getCert = async ({ id, serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -117,13 +120,14 @@ export const certificateServiceFactory = ({
* Get certificate private key. * Get certificate private key.
*/ */
const getCertPrivateKey = async ({ const getCertPrivateKey = async ({
id,
serialNumber, serialNumber,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
actorOrgId actorOrgId
}: TGetCertPrivateKeyDTO) => { }: TGetCertPrivateKeyDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -156,8 +160,8 @@ export const certificateServiceFactory = ({
/** /**
* Delete certificate with serial number [serialNumber] * Delete certificate with serial number [serialNumber]
*/ */
const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => { const deleteCert = async ({ id, serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -193,6 +197,7 @@ export const certificateServiceFactory = ({
* of its issuing CA * of its issuing CA
*/ */
const revokeCert = async ({ const revokeCert = async ({
id,
serialNumber, serialNumber,
revocationReason, revocationReason,
actorId, actorId,
@@ -200,7 +205,7 @@ export const certificateServiceFactory = ({
actor, actor,
actorOrgId actorOrgId
}: TRevokeCertDTO) => { }: TRevokeCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
if (!cert.caId) { if (!cert.caId) {
throw new BadRequestError({ throw new BadRequestError({
@@ -290,8 +295,8 @@ export const certificateServiceFactory = ({
* Return certificate body and certificate chain for certificate with * Return certificate body and certificate chain for certificate with
* serial number [serialNumber] * serial number [serialNumber]
*/ */
const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => { const getCertBody = async ({ id, serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -584,8 +589,15 @@ export const certificateServiceFactory = ({
* Return certificate body and certificate chain for certificate with * Return certificate body and certificate chain for certificate with
* serial number [serialNumber] * serial number [serialNumber]
*/ */
const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => { const getCertBundle = async ({
const cert = await certificateDAL.findOne({ serialNumber }); id,
serialNumber,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetCertBundleDTO) => {
const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -673,12 +685,13 @@ export const certificateServiceFactory = ({
certificate, certificate,
certificateChain, certificateChain,
privateKey, privateKey,
serialNumber, serialNumber: cert.serialNumber,
cert cert
}; };
}; };
const getCertPkcs12 = async ({ const getCertPkcs12 = async ({
id,
serialNumber, serialNumber,
password, password,
alias, alias,
@@ -700,7 +713,7 @@ export const certificateServiceFactory = ({
if (!alias || alias.trim() === "") { if (!alias || alias.trim() === "") {
throw new BadRequestError({ message: "Alias is required for PKCS12 keystore generation" }); throw new BadRequestError({ message: "Alias is required for PKCS12 keystore generation" });
} }
const cert = await certificateDAL.findOne({ serialNumber }); const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -718,7 +731,7 @@ export const certificateServiceFactory = ({
// Get certificate bundle (certificate, chain, private key) // Get certificate bundle (certificate, chain, private key)
const { certificate, certificateChain, privateKey } = await getCertBundle({ const { certificate, certificateChain, privateKey } = await getCertBundle({
serialNumber, id: cert.id,
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
@@ -84,20 +84,24 @@ export enum CrlReason {
} }
export type TGetCertDTO = { export type TGetCertDTO = {
serialNumber: string; id?: string;
serialNumber?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TDeleteCertDTO = { export type TDeleteCertDTO = {
serialNumber: string; id?: string;
serialNumber?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TRevokeCertDTO = { export type TRevokeCertDTO = {
serialNumber: string; id?: string;
serialNumber?: string;
revocationReason: CrlReason; revocationReason: CrlReason;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetCertBodyDTO = { export type TGetCertBodyDTO = {
serialNumber: string; id?: string;
serialNumber?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TImportCertDTO = { export type TImportCertDTO = {
@@ -112,15 +116,18 @@ export type TImportCertDTO = {
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetCertPrivateKeyDTO = { export type TGetCertPrivateKeyDTO = {
serialNumber: string; id?: string;
serialNumber?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetCertBundleDTO = { export type TGetCertBundleDTO = {
serialNumber: string; id?: string;
serialNumber?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetCertPkcs12DTO = { export type TGetCertPkcs12DTO = {
serialNumber: string; id?: string;
serialNumber?: string;
password: string; password: string;
alias: string; alias: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -524,8 +524,8 @@ export const pkiSubscriberServiceFactory = ({
}); });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
@@ -466,8 +466,8 @@ export const pkiTemplatesServiceFactory = ({
}); });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`; const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
@@ -1,4 +1,4 @@
--- ---
title: "Create" title: "Create"
openapi: "POST /api/v1/pki/ca/acme" openapi: "POST /api/v1/cert-manager/ca/acme"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Delete" title: "Delete"
openapi: "DELETE /api/v1/pki/ca/acme/{caName}" openapi: "DELETE /api/v1/cert-manager/ca/acme/{id}"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "List" title: "List"
openapi: "GET /api/v1/pki/ca/acme" openapi: "GET /api/v1/cert-manager/ca/acme"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Read" title: "Read"
openapi: "GET /api/v1/pki/ca/acme/{caName}" openapi: "GET /api/v1/cert-manager/ca/acme/{id}"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Update" title: "Update"
openapi: "PATCH /api/v1/pki/ca/acme/{caName}" openapi: "PATCH /api/v1/cert-manager/ca/acme/{id}"
--- ---
@@ -1,4 +0,0 @@
---
title: "Retrieve certificate / chain"
openapi: "GET /api/v1/pki/ca/{caId}/certificate"
---
@@ -1,8 +0,0 @@
---
title: "Create (Deprecated)"
openapi: "POST /api/v1/pki/ca"
---
<Note>
This endpoint is deprecated. Please use the internal CA endpoint [here](/api-reference/endpoints/certificate-authorities/internal/create).
</Note>
@@ -1,4 +0,0 @@
---
title: "List CRLs"
openapi: "GET /api/v1/pki/ca/{caId}/crls"
---
@@ -1,4 +0,0 @@
---
title: "Get CSR"
openapi: "GET /api/v1/pki/ca/{caId}/csr"
---
@@ -1,8 +0,0 @@
---
title: "Delete (Deprecated)"
openapi: "DELETE /api/v1/pki/ca/{caId}"
---
<Note>
This endpoint is deprecated. Please use the internal CA endpoint [here](/api-reference/endpoints/certificate-authorities/internal/delete).
</Note>
@@ -1,4 +0,0 @@
---
title: "Import certificate"
openapi: "POST /api/v1/pki/ca/{caId}/import-certificate"
---
@@ -0,0 +1,4 @@
---
title: "Retrieve certificate / chain"
openapi: "GET /api/v1/cert-manager/ca/internal/{caId}/certificate"
---
@@ -1,4 +1,4 @@
--- ---
title: "Create" title: "Create"
openapi: "POST /api/v1/pki/ca/internal" openapi: "POST /api/v1/cert-manager/ca/internal"
--- ---
@@ -0,0 +1,4 @@
---
title: "List CRLs"
openapi: "GET /api/v1/cert-manager/ca/internal/{caId}/crls"
---
@@ -0,0 +1,4 @@
---
title: "Get CSR"
openapi: "GET /api/v1/cert-manager/ca/internal/{caId}/csr"
---
@@ -1,4 +1,4 @@
--- ---
title: "Delete" title: "Delete"
openapi: "DELETE /api/v1/pki/ca/internal/{caName}" openapi: "DELETE /api/v1/cert-manager/ca/internal/{id}"
--- ---
@@ -0,0 +1,4 @@
---
title: "Import certificate"
openapi: "POST /api/v1/cert-manager/ca/internal/{caId}/import-certificate"
---
@@ -0,0 +1,4 @@
---
title: "List CA certificates"
openapi: "GET /api/v1/cert-manager/ca/internal/{caId}/ca-certificates"
---
@@ -1,4 +1,4 @@
--- ---
title: "List" title: "List"
openapi: "GET /api/v1/pki/ca/internal" openapi: "GET /api/v1/cert-manager/ca/internal"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Read" title: "Read"
openapi: "GET /api/v1/pki/ca/internal/{caName}" openapi: "GET /api/v1/cert-manager/ca/internal/{id}"
--- ---
@@ -0,0 +1,4 @@
---
title: "Renew"
openapi: "POST /api/v1/cert-manager/ca/internal/{caId}/renew"
---
@@ -0,0 +1,4 @@
---
title: "Sign intermediate certificate"
openapi: "POST /api/v1/cert-manager/ca/internal/{caId}/sign-intermediate"
---
@@ -1,4 +1,4 @@
--- ---
title: "Update" title: "Update"
openapi: "PATCH /api/v1/pki/ca/internal/{caName}" openapi: "PATCH /api/v1/cert-manager/ca/internal/{id}"
--- ---
@@ -1,4 +0,0 @@
---
title: "List CA certificates"
openapi: "GET /api/v1/pki/ca/{caId}/ca-certificates"
---
@@ -1,8 +0,0 @@
---
title: "List (Deprecated)"
openapi: "GET /api/v2/workspace/{slug}/cas"
---
<Note>
This endpoint is deprecated. Please use the internal CA endpoint [here](/api-reference/endpoints/certificate-authorities/internal/list).
</Note>
@@ -1,8 +0,0 @@
---
title: "Retrieve (Deprecated)"
openapi: "GET /api/v1/pki/ca/{caId}"
---
<Note>
This endpoint is deprecated. Please use the internal CA endpoint [here](/api-reference/endpoints/certificate-authorities/internal/read).
</Note>
@@ -1,4 +0,0 @@
---
title: "Renew"
openapi: "POST /api/v1/pki/ca/{caId}/renew"
---
@@ -1,4 +0,0 @@
---
title: "Sign intermediate certificate"
openapi: "POST /api/v1/pki/ca/{caId}/sign-intermediate"
---
@@ -1,8 +0,0 @@
---
title: "Update (Deprecated)"
openapi: "PATCH /api/v1/pki/ca/{caId}"
---
<Note>
This endpoint is deprecated. Please use the internal CA endpoint [here](/api-reference/endpoints/certificate-authorities/internal/update).
</Note>
@@ -1,4 +1,4 @@
--- ---
title: "Create" title: "Create"
openapi: "POST /api/v1/pki/certificate-profiles" openapi: "POST /api/v1/cert-manager/certificate-profiles"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Delete" title: "Delete"
openapi: "DELETE /api/v1/pki/certificate-profiles/{id}" openapi: "DELETE /api/v1/cert-manager/certificate-profiles/{id}"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Get by ID" title: "Get by ID"
openapi: "GET /api/v1/pki/certificate-profiles/{id}" openapi: "GET /api/v1/cert-manager/certificate-profiles/{id}"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Get by Slug" title: "Get by Slug"
openapi: "GET /api/v1/pki/certificate-profiles/slug/{slug}" openapi: "GET /api/v1/cert-manager/certificate-profiles/slug/{slug}"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Get Latest Active Certificate Bundle" title: "Get Latest Active Certificate Bundle"
openapi: "GET /api/v1/pki/certificate-profiles/{id}/certificates/latest-active-bundle" openapi: "GET /api/v1/cert-manager/certificate-profiles/{id}/certificates/latest-active-bundle"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "List Certificates" title: "List Certificates"
openapi: "GET /api/v1/pki/certificate-profiles/{id}/certificates" openapi: "GET /api/v1/cert-manager/certificate-profiles/{id}/certificates"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "List" title: "List"
openapi: "GET /api/v1/pki/certificate-profiles" openapi: "GET /api/v1/cert-manager/certificate-profiles"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Update" title: "Update"
openapi: "PATCH /api/v1/pki/certificate-profiles/{id}" openapi: "PATCH /api/v1/cert-manager/certificate-profiles/{id}"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "List PKI Syncs" title: "List PKI Syncs"
openapi: "GET /api/v1/pki/syncs" openapi: "GET /api/v1/cert-manager/syncs"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Options" title: "Options"
openapi: "GET /api/v1/pki/syncs/options" openapi: "GET /api/v1/cert-manager/syncs/options"
--- ---
@@ -1,4 +0,0 @@
---
title: "Create"
openapi: "POST /api/v2/certificate-templates"
---
@@ -1,4 +0,0 @@
---
title: "Delete"
openapi: "DELETE /api/v2/certificate-templates/{id}"
---

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