mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-10 07:58:15 -05:00
Merge remote-tracking branch 'origin/main' into feat/PKI-55
This commit is contained in:
15
.env.example
15
.env.example
@@ -31,25 +31,14 @@ SMTP_FROM_NAME=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
|
||||
# Integration
|
||||
# Optional only if integration is used
|
||||
CLIENT_ID_HEROKU=
|
||||
CLIENT_ID_VERCEL=
|
||||
CLIENT_ID_NETLIFY=
|
||||
# CICD Integration
|
||||
CLIENT_ID_GITHUB=
|
||||
CLIENT_ID_GITHUB_APP=
|
||||
CLIENT_SLUG_GITHUB_APP=
|
||||
CLIENT_ID_GITLAB=
|
||||
CLIENT_ID_BITBUCKET=
|
||||
CLIENT_SECRET_HEROKU=
|
||||
CLIENT_SECRET_VERCEL=
|
||||
CLIENT_SECRET_NETLIFY=
|
||||
CLIENT_SECRET_GITHUB=
|
||||
CLIENT_SECRET_GITHUB_APP=
|
||||
CLIENT_ID_GITLAB=
|
||||
CLIENT_SECRET_GITLAB=
|
||||
CLIENT_SECRET_BITBUCKET=
|
||||
CLIENT_SLUG_VERCEL=
|
||||
|
||||
CLIENT_PRIVATE_KEY_GITHUB_APP=
|
||||
CLIENT_APP_ID_GITHUB_APP=
|
||||
|
||||
|
||||
36
.github/pull_request_template.md
vendored
36
.github/pull_request_template.md
vendored
@@ -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
|
||||
- [ ] New feature
|
||||
<!-- If UI/UX changes, add screenshots or videos. Delete if not applicable. -->
|
||||
|
||||
## Steps to verify the change
|
||||
|
||||
## Type
|
||||
|
||||
- [ ] Fix
|
||||
- [ ] Feature
|
||||
- [ ] Improvement
|
||||
- [ ] Breaking change
|
||||
- [ ] Documentation
|
||||
- [ ] Breaking
|
||||
- [ ] 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 -->
|
||||
|
||||
```sh
|
||||
# Here's some code block to paste some code snippets
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
- [ ] I have read the [contributing guide](https://infisical.com/docs/contributing/getting-started/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/getting-started/code-of-conduct). 📝
|
||||
- [ ] Title follows the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) format: `type(scope): short description` (scope is optional, e.g., `fix: prevent crash on sync` or `fix(api): handle null response`).
|
||||
- [ ] Tested locally
|
||||
- [ ] Updated docs (if needed)
|
||||
- [ ] Read the [contributing guide](https://infisical.com/docs/contributing/getting-started/overview)
|
||||
55
.github/workflows/validate-pr-title.yml
vendored
Normal file
55
.github/workflows/validate-pr-title.yml
vendored
Normal 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}"`);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||
ARG INTERCOM_ID
|
||||
ENV VITE_INTERCOM_ID $INTERCOM_ID
|
||||
ARG INFISICAL_PLATFORM_VERSION
|
||||
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||
ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||
ARG CAPTCHA_SITE_KEY
|
||||
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||
|
||||
@@ -87,14 +87,13 @@ def bootstrap_infisical(context: Context):
|
||||
|
||||
ca_slug = faker.slug()
|
||||
resp = client.post(
|
||||
"/api/v1/pki/ca/internal",
|
||||
"/api/v1/cert-manager/ca/internal",
|
||||
headers=headers,
|
||||
json={
|
||||
"projectId": project["id"],
|
||||
"name": ca_slug,
|
||||
"type": "internal",
|
||||
"status": "active",
|
||||
"enableDirectIssuance": True,
|
||||
"configuration": {
|
||||
"type": "root",
|
||||
"organization": "Infisican Inc",
|
||||
@@ -115,7 +114,7 @@ def bootstrap_infisical(context: Context):
|
||||
|
||||
cert_template_slug = faker.slug()
|
||||
resp = client.post(
|
||||
"/api/v2/certificate-templates",
|
||||
"/api/v1/cert-manager/certificate-templates",
|
||||
headers=headers,
|
||||
json={
|
||||
"projectId": project["id"],
|
||||
|
||||
@@ -2,7 +2,7 @@ Feature: Access Control
|
||||
|
||||
Scenario Outline: Access resources across different account
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0
|
||||
Then I memorize acme_account0.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account0_id
|
||||
When I create certificate signing request as csr
|
||||
@@ -34,7 +34,7 @@ Feature: Access Control
|
||||
Then the value response.status_code should not be equal to 404
|
||||
And I put away current ACME client as client0
|
||||
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email maidu@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account1
|
||||
Then I peak and memorize the next nonce as nonce
|
||||
When I send a raw ACME request to "<url>"
|
||||
@@ -53,7 +53,7 @@ Feature: Access Control
|
||||
|
||||
Examples: Endpoints
|
||||
| src_var | jq | dest_var | url | payload |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | |
|
||||
| order | . | not_used | {order.uri} | |
|
||||
| order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} |
|
||||
| order | . | not_used | {order.uri}/certificate | |
|
||||
@@ -62,7 +62,7 @@ Feature: Access Control
|
||||
|
||||
Scenario Outline: Access resources across a different profiles
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0
|
||||
Then I memorize acme_account0.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account0_id
|
||||
When I create certificate signing request as csr
|
||||
@@ -96,7 +96,7 @@ Feature: Access Control
|
||||
|
||||
Given I make a random slug as profile_slug
|
||||
Given I use AUTH_TOKEN for authentication
|
||||
When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload
|
||||
When I send a "POST" request to "/api/v1/cert-manager/certificate-profiles" with JSON payload
|
||||
"""
|
||||
{
|
||||
"projectId": "{PROJECT_ID}",
|
||||
@@ -110,10 +110,10 @@ Feature: Access Control
|
||||
"""
|
||||
Then the value response.status_code should be equal to 200
|
||||
Then I memorize response with jq ".certificateProfile.id" as profile_id
|
||||
When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal"
|
||||
When I send a "GET" request to "/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal"
|
||||
Then I memorize response with jq ".eabKid" as eab_kid
|
||||
And I memorize response with jq ".eabSecret" as eab_secret
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{profile_id}/directory"
|
||||
Then I register a new ACME account with email maidu@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account1
|
||||
Then I peak and memorize the next nonce as nonce
|
||||
Then I memorize <src_var> with jq "<jq>" as <dest_var>
|
||||
@@ -133,7 +133,7 @@ Feature: Access Control
|
||||
|
||||
Examples: Endpoints
|
||||
| src_var | jq | dest_var | url | payload |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | |
|
||||
| order | . | not_used | {order.uri} | |
|
||||
| order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} |
|
||||
| order | . | not_used | {order.uri}/certificate | |
|
||||
@@ -143,7 +143,7 @@ Feature: Access Control
|
||||
|
||||
Scenario Outline: Access resources across a different profile with the same key pair
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0
|
||||
Then I memorize acme_account0.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account0_id
|
||||
When I create certificate signing request as csr
|
||||
@@ -177,7 +177,7 @@ Feature: Access Control
|
||||
|
||||
Given I make a random slug as profile_slug
|
||||
Given I use AUTH_TOKEN for authentication
|
||||
When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload
|
||||
When I send a "POST" request to "/api/v1/cert-manager/certificate-profiles" with JSON payload
|
||||
"""
|
||||
{
|
||||
"projectId": "{PROJECT_ID}",
|
||||
@@ -191,10 +191,10 @@ Feature: Access Control
|
||||
"""
|
||||
Then the value response.status_code should be equal to 200
|
||||
Then I memorize response with jq ".certificateProfile.id" as profile_id
|
||||
When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal"
|
||||
When I send a "GET" request to "/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal"
|
||||
Then I memorize response with jq ".eabKid" as eab_kid
|
||||
And I memorize response with jq ".eabSecret" as eab_secret
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory" with the key pair from client0
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{profile_id}/directory" with the key pair from client0
|
||||
Then I register a new ACME account with email maidu@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account1
|
||||
Then I peak and memorize the next nonce as nonce
|
||||
Then I memorize <src_var> with jq "<jq>" as <dest_var>
|
||||
@@ -214,7 +214,7 @@ Feature: Access Control
|
||||
|
||||
Examples: Endpoints
|
||||
| src_var | jq | dest_var | url | payload |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | |
|
||||
| order | . | not_used | {order.uri} | |
|
||||
| order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} |
|
||||
| order | . | not_used | {order.uri}/certificate | |
|
||||
@@ -223,7 +223,7 @@ Feature: Access Control
|
||||
|
||||
Scenario Outline: URL mismatch
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
Then I memorize acme_account.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account_id
|
||||
When I create certificate signing request as csr
|
||||
@@ -258,8 +258,8 @@ Feature: Access Control
|
||||
|
||||
Examples: Endpoints
|
||||
| src_var | jq | dest_var | actual_url | bad_url | error_detail |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | BAD | Invalid URL in the protected header |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | https://evil.com/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | URL mismatch in the protected header |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | BAD | Invalid URL in the protected header |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | https://evil.com/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | URL mismatch in the protected header |
|
||||
| order | . | not_used | {order.uri} | BAD | Invalid URL in the protected header |
|
||||
| order | . | not_used | {order.uri} | https://example.com/acmes/orders/FOOBAR | URL mismatch in the protected header |
|
||||
| order | . | not_used | {order.uri}/finalize | BAD | Invalid URL in the protected header |
|
||||
@@ -273,7 +273,7 @@ Feature: Access Control
|
||||
|
||||
Scenario Outline: Send KID and JWK in the same time
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
And I memorize acme_account.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account_id
|
||||
When I create certificate signing request as csr
|
||||
@@ -312,8 +312,8 @@ Feature: Access Control
|
||||
|
||||
Examples: Endpoints
|
||||
| src_var | jq | dest_var | url |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order |
|
||||
| order | . | not_used | {order.uri} |
|
||||
| order | . | not_used | {order.uri}/finalize |
|
||||
| order | . | not_used | {order.uri}/certificate |
|
||||
|
||||
@@ -2,13 +2,13 @@ Feature: Account
|
||||
|
||||
Scenario: Create a new account
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
And the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/(.+)
|
||||
And the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/(.+)
|
||||
|
||||
Scenario: Create a new account with the same key pair twice
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
And I memorize acme_account.uri as kid
|
||||
And I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account2
|
||||
@@ -17,7 +17,7 @@ Feature: Account
|
||||
|
||||
Scenario: Find an existing account
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
And I memorize acme_account.uri as account_uri
|
||||
And I find the existing ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as retrieved_account
|
||||
@@ -26,7 +26,7 @@ Feature: Account
|
||||
# Note: This is a very special case for cert-manager.
|
||||
Scenario: Create a new account with EAB then retrieve it without EAB
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
And I memorize acme_account.uri as account_uri
|
||||
And I find the existing ACME account without EAB as retrieved_account
|
||||
@@ -35,13 +35,13 @@ Feature: Account
|
||||
|
||||
Scenario: Create a new account without EAB
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com without EAB
|
||||
And the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired"
|
||||
|
||||
Scenario Outline: Scenario: Create a new account with bad EAB credentials
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "<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 ".detail" should be equal to "<error_msg>"
|
||||
@@ -57,17 +57,17 @@ Feature: Account
|
||||
|
||||
Scenario Outline: Scenario: Create a new account with bad EAB url
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
And I use a different new-account URL "<url>" for EAB signature
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
And the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired"
|
||||
And the value error with jq ".detail" should be equal to "External account binding URL mismatch"
|
||||
|
||||
Examples: Bad URLs
|
||||
| url |
|
||||
| {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account-bad |
|
||||
| {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account?foo=bar |
|
||||
| {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account#foobar |
|
||||
| {BASE_URL}/acme/new-account |
|
||||
| https://example.com/api/v1/pki/acme/profiles/{acme_profile.id}/new-account-bad |
|
||||
| bad |
|
||||
| url |
|
||||
| {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account-bad |
|
||||
| {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account?foo=bar |
|
||||
| {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account#foobar |
|
||||
| {BASE_URL}/acme/new-account |
|
||||
| https://example.com/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account-bad |
|
||||
| bad |
|
||||
|
||||
@@ -2,7 +2,7 @@ Feature: Authorization
|
||||
|
||||
Scenario: Get authorization
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
@@ -14,7 +14,7 @@ Feature: Authorization
|
||||
Then I create a RSA private key pair as cert_key
|
||||
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
|
||||
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
|
||||
And the value order.authorizations[0].uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+)
|
||||
And the value order.authorizations[0].uri with jq "." should match pattern {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/authorizations/(.+)
|
||||
And the value order.authorizations[0].body with jq ".status" should be equal to "pending"
|
||||
And the value order.authorizations[0].body with jq ".challenges | map(pick(.type, .status)) | sort_by(.type)" should be equal to json
|
||||
"""
|
||||
|
||||
@@ -3,7 +3,7 @@ Feature: ACME Cert Profile
|
||||
Scenario: Create a cert profile
|
||||
Given I make a random slug as profile_slug
|
||||
And I use AUTH_TOKEN for authentication
|
||||
When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload
|
||||
When I send a "POST" request to "/api/v1/cert-manager/certificate-profiles" with JSON payload
|
||||
"""
|
||||
{
|
||||
"projectId": "{PROJECT_ID}",
|
||||
@@ -25,7 +25,7 @@ Feature: ACME Cert Profile
|
||||
Scenario: Reveal EAB secret
|
||||
Given I make a random slug as profile_slug
|
||||
And I use AUTH_TOKEN for authentication
|
||||
When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload
|
||||
When I send a "POST" request to "/api/v1/cert-manager/certificate-profiles" with JSON payload
|
||||
"""
|
||||
{
|
||||
"projectId": "{PROJECT_ID}",
|
||||
@@ -39,11 +39,11 @@ Feature: ACME Cert Profile
|
||||
"""
|
||||
Then the value response.status_code should be equal to 200
|
||||
And I memorize response with jq ".certificateProfile.id" as profile_id
|
||||
When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal"
|
||||
When I send a "GET" request to "/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal"
|
||||
Then the value response.status_code should be equal to 200
|
||||
And the value response with jq ".eabKid" should be equal to "{profile_id}"
|
||||
And the value response with jq ".eabSecret" should be present
|
||||
And I memorize response with jq ".eabKid" as eab_kid
|
||||
And I memorize response with jq ".eabSecret" as eab_secret
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{profile_id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account
|
||||
|
||||
@@ -2,7 +2,7 @@ Feature: Challenge
|
||||
|
||||
Scenario: Validate challenge
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
@@ -24,7 +24,7 @@ Feature: Challenge
|
||||
|
||||
Scenario: Validate challenges for multiple domains
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
@@ -58,7 +58,7 @@ Feature: Challenge
|
||||
|
||||
Scenario: Did not finish all challenges
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
@@ -153,7 +153,7 @@ Feature: Challenge
|
||||
|
||||
Scenario: CSR names mismatch with order identifier
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
@@ -165,13 +165,13 @@ Feature: Challenge
|
||||
And I create a RSA private key pair as cert_key
|
||||
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
|
||||
Then I peak and memorize the next nonce as nonce
|
||||
When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order"
|
||||
When I send a raw ACME request to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order"
|
||||
"""
|
||||
{
|
||||
"protected": {
|
||||
"alg": "RS256",
|
||||
"nonce": "{nonce}",
|
||||
"url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order",
|
||||
"url": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order",
|
||||
"kid": "{acme_account.uri}"
|
||||
},
|
||||
"payload": {
|
||||
|
||||
@@ -2,14 +2,14 @@ Feature: Directory
|
||||
|
||||
Scenario: Get the directory of ACME service urls
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I send a "GET" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I send a "GET" request to "/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then the response status code should be "200"
|
||||
And the response body should match JSON value
|
||||
"""
|
||||
{
|
||||
"newNonce": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce",
|
||||
"newAccount": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account",
|
||||
"newOrder": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order",
|
||||
"newNonce": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-nonce",
|
||||
"newAccount": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-account",
|
||||
"newOrder": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order",
|
||||
"meta": {
|
||||
"externalAccountRequired": true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
Feature: External CA
|
||||
|
||||
Scenario: Issue a certificate from an external CA
|
||||
@cloudflare
|
||||
Scenario Outline: Issue a certificate from an external CA with Cloudflare
|
||||
Given I create a Cloudflare connection as cloudflare
|
||||
Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id
|
||||
Given I create a external ACME CA with the following config as ext_ca
|
||||
@@ -87,14 +88,12 @@ Feature: External CA
|
||||
"""
|
||||
Then I memorize cert_template with jq ".certificateTemplate.id" as cert_template_id
|
||||
Given I create an ACME profile with ca {ext_ca_id} and template {cert_template_id} as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
"""
|
||||
{
|
||||
"COMMON_NAME": "localhost"
|
||||
}
|
||||
<subject>
|
||||
"""
|
||||
# Pebble has a strict rule to only takes SANs
|
||||
Then I add subject alternative name to certificate signing request csr
|
||||
@@ -177,4 +176,196 @@ Feature: External CA
|
||||
[
|
||||
"localhost"
|
||||
]
|
||||
"""
|
||||
"""
|
||||
|
||||
Examples:
|
||||
| subject |
|
||||
| {"COMMON_NAME": "localhost"} |
|
||||
| {} |
|
||||
|
||||
@dnsme
|
||||
Scenario Outline: Issue a certificate from an external CA with DNS Made Easy
|
||||
Given I create a DNS Made Easy connection as dnsme
|
||||
Then I memorize dnsme with jq ".appConnection.id" as app_conn_id
|
||||
Given I create a external ACME CA with the following config as ext_ca
|
||||
"""
|
||||
{
|
||||
"dnsProviderConfig": {
|
||||
"provider": "dns-made-easy",
|
||||
"hostedZoneId": "MOCK_ZONE_ID"
|
||||
},
|
||||
"directoryUrl": "{PEBBLE_URL}",
|
||||
"accountEmail": "fangpen@infisical.com",
|
||||
"dnsAppConnectionId": "{app_conn_id}",
|
||||
"eabKid": "",
|
||||
"eabHmacKey": ""
|
||||
}
|
||||
"""
|
||||
Then I memorize ext_ca with jq ".id" as ext_ca_id
|
||||
Given I create a certificate template with the following config as cert_template
|
||||
"""
|
||||
{
|
||||
"subject": [
|
||||
{
|
||||
"type": "common_name",
|
||||
"allowed": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"sans": [
|
||||
{
|
||||
"type": "dns_name",
|
||||
"allowed": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"keyUsages": {
|
||||
"required": [],
|
||||
"allowed": [
|
||||
"digital_signature",
|
||||
"key_encipherment",
|
||||
"non_repudiation",
|
||||
"data_encipherment",
|
||||
"key_agreement",
|
||||
"key_cert_sign",
|
||||
"crl_sign",
|
||||
"encipher_only",
|
||||
"decipher_only"
|
||||
]
|
||||
},
|
||||
"extendedKeyUsages": {
|
||||
"required": [],
|
||||
"allowed": [
|
||||
"client_auth",
|
||||
"server_auth",
|
||||
"code_signing",
|
||||
"email_protection",
|
||||
"ocsp_signing",
|
||||
"time_stamping"
|
||||
]
|
||||
},
|
||||
"algorithms": {
|
||||
"signature": [
|
||||
"SHA256-RSA",
|
||||
"SHA512-RSA",
|
||||
"SHA384-ECDSA",
|
||||
"SHA384-RSA",
|
||||
"SHA256-ECDSA",
|
||||
"SHA512-ECDSA"
|
||||
],
|
||||
"keyAlgorithm": [
|
||||
"RSA-2048",
|
||||
"RSA-4096",
|
||||
"ECDSA-P384",
|
||||
"RSA-3072",
|
||||
"ECDSA-P256",
|
||||
"ECDSA-P521"
|
||||
]
|
||||
},
|
||||
"validity": {
|
||||
"max": "365d"
|
||||
}
|
||||
}
|
||||
"""
|
||||
Then I memorize cert_template with jq ".certificateTemplate.id" as cert_template_id
|
||||
Given I create an ACME profile with ca {ext_ca_id} and template {cert_template_id} as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
"""
|
||||
<subject>
|
||||
"""
|
||||
# Pebble has a strict rule to only takes SANs
|
||||
Then I add subject alternative name to certificate signing request csr
|
||||
"""
|
||||
[
|
||||
"localhost"
|
||||
]
|
||||
"""
|
||||
And I create a RSA private key pair as cert_key
|
||||
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
|
||||
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
|
||||
And I select challenge with type http-01 for domain localhost from order in order as challenge
|
||||
And I serve challenge response for challenge at localhost
|
||||
And I tell ACME server that challenge is ready to be verified
|
||||
Given I intercept outgoing requests
|
||||
"""
|
||||
[
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "POST",
|
||||
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records",
|
||||
"status": 201,
|
||||
"response": {
|
||||
"gtdLocation": "DEFAULT",
|
||||
"failed": false,
|
||||
"monitor": false,
|
||||
"failover": false,
|
||||
"sourceId": 895364,
|
||||
"dynamicDns": false,
|
||||
"hardLink": false,
|
||||
"ttl": 60,
|
||||
"source": 1,
|
||||
"name": "_acme-challenge",
|
||||
"value": "\"MOCK_HTTP_01_VALUE\"",
|
||||
"id": 12345678,
|
||||
"type": "TXT"
|
||||
},
|
||||
"responseIsBinary": false
|
||||
},
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "GET",
|
||||
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records?type=TXT&recordName=_acme-challenge&page=0",
|
||||
"status": 200,
|
||||
"response": {
|
||||
"totalRecords": 1,
|
||||
"totalPages": 1,
|
||||
"data": [
|
||||
{
|
||||
"gtdLocation": "DEFAULT",
|
||||
"failed": false,
|
||||
"monitor": false,
|
||||
"failover": false,
|
||||
"sourceId": 895364,
|
||||
"dynamicDns": false,
|
||||
"hardLink": false,
|
||||
"ttl": 60,
|
||||
"source": 1,
|
||||
"name": "_acme-challenge",
|
||||
"value": "\"MOCK_CHALLENGE_VALUE\"",
|
||||
"id": 1111111,
|
||||
"type": "TXT"
|
||||
}
|
||||
],
|
||||
"page": 0
|
||||
},
|
||||
"responseIsBinary": false
|
||||
},
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "DELETE",
|
||||
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records/1111111",
|
||||
"status": 200,
|
||||
"response": "",
|
||||
"responseIsBinary": false
|
||||
}
|
||||
]
|
||||
"""
|
||||
Then I poll and finalize the ACME order order as finalized_order
|
||||
And the value finalized_order.body with jq ".status" should be equal to "valid"
|
||||
And I parse the full-chain certificate from order finalized_order as cert
|
||||
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
|
||||
"""
|
||||
[
|
||||
"localhost"
|
||||
]
|
||||
"""
|
||||
|
||||
Examples:
|
||||
| subject |
|
||||
| {"COMMON_NAME": "localhost"} |
|
||||
| {} |
|
||||
|
||||
@@ -2,7 +2,7 @@ Feature: Internal CA
|
||||
|
||||
Scenario: CSR with SANs only
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
|
||||
@@ -2,13 +2,13 @@ Feature: Nonce
|
||||
|
||||
Scenario: Generate a new nonce
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I send a "HEAD" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce"
|
||||
When I send a "HEAD" request to "/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-nonce"
|
||||
Then the response status code should be "200"
|
||||
And the response header "Replay-Nonce" should contains non-empty value
|
||||
|
||||
Scenario Outline: Send a bad nonce to account endpoints
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
And I memorize acme_account.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account_id
|
||||
When I create certificate signing request as csr
|
||||
@@ -40,18 +40,18 @@ Feature: Nonce
|
||||
And the value response with jq ".detail" should be equal to "Invalid nonce"
|
||||
|
||||
Examples: Endpoints
|
||||
| src_var | jq | dest_var | url |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order |
|
||||
| order | . | not_used | {order.uri} |
|
||||
| order | . | not_used | {order.uri}/finalize |
|
||||
| order | . | not_used | {order.uri}/certificate |
|
||||
| order | .authorizations[0].uri | auth_uri | {auth_uri} |
|
||||
| order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} |
|
||||
| src_var | jq | dest_var | url |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order |
|
||||
| order | . | not_used | {order.uri} |
|
||||
| order | . | not_used | {order.uri}/finalize |
|
||||
| order | . | not_used | {order.uri}/certificate |
|
||||
| order | .authorizations[0].uri | auth_uri | {auth_uri} |
|
||||
| order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} |
|
||||
|
||||
Scenario Outline: Send the same nonce twice
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
And I memorize acme_account.uri with jq "capture("/(?<id>[^/]+)$") | .id" as account_id
|
||||
When I create certificate signing request as csr
|
||||
@@ -65,13 +65,13 @@ Feature: Nonce
|
||||
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
|
||||
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
|
||||
And I peak and memorize the next nonce as nonce_value
|
||||
When I send a raw ACME request to "/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders"
|
||||
When I send a raw ACME request to "/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders"
|
||||
"""
|
||||
{
|
||||
"protected": {
|
||||
"alg": "RS256",
|
||||
"nonce": "{nonce_value}",
|
||||
"url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders",
|
||||
"url": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders",
|
||||
"kid": "{acme_account.uri}"
|
||||
},
|
||||
"payload": {}
|
||||
@@ -97,11 +97,11 @@ Feature: Nonce
|
||||
And the value response with jq ".detail" should be equal to "Invalid nonce"
|
||||
|
||||
Examples: Endpoints
|
||||
| src_var | jq | dest_var | url |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order |
|
||||
| order | . | not_used | {order.uri} |
|
||||
| order | . | not_used | {order.uri}/finalize |
|
||||
| order | . | not_used | {order.uri}/certificate |
|
||||
| order | .authorizations[0].uri | auth_uri | {auth_uri} |
|
||||
| order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} |
|
||||
| src_var | jq | dest_var | url |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders |
|
||||
| order | . | not_used | {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order |
|
||||
| order | . | not_used | {order.uri} |
|
||||
| order | . | not_used | {order.uri}/finalize |
|
||||
| order | . | not_used | {order.uri}/certificate |
|
||||
| order | .authorizations[0].uri | auth_uri | {auth_uri} |
|
||||
| order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} |
|
||||
|
||||
@@ -2,7 +2,7 @@ Feature: Order
|
||||
|
||||
Scenario: Create a new order
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
@@ -14,15 +14,15 @@ Feature: Order
|
||||
Then I create a RSA private key pair as cert_key
|
||||
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
|
||||
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
|
||||
And the value order.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)
|
||||
And the value order.uri with jq "." should match pattern {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/orders/(.+)
|
||||
And the value order.body with jq ".status" should be equal to "pending"
|
||||
And the value order.body with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}]
|
||||
And the value order.body with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize
|
||||
And the value order.body with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true
|
||||
And the value order.body with jq ".finalize" should match pattern {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/orders/(.+)/finalize
|
||||
And the value order.body with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true
|
||||
|
||||
Scenario: Create a new order with SANs
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
@@ -52,7 +52,7 @@ Feature: Order
|
||||
|
||||
Scenario: Fetch an order
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
@@ -67,21 +67,21 @@ Feature: Order
|
||||
And I send an ACME post-as-get to order.uri as fetched_order
|
||||
And the value fetched_order with jq ".status" should be equal to "pending"
|
||||
And the value fetched_order with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}]
|
||||
And the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize
|
||||
And the value fetched_order with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true
|
||||
And the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/orders/(.+)/finalize
|
||||
And the value fetched_order with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true
|
||||
|
||||
Scenario Outline: Create an order with invalid identifier types
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
And I peak and memorize the next nonce as nonce
|
||||
When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order"
|
||||
When I send a raw ACME request to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order"
|
||||
"""
|
||||
{
|
||||
"protected": {
|
||||
"alg": "RS256",
|
||||
"nonce": "{nonce}",
|
||||
"url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order",
|
||||
"url": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order",
|
||||
"kid": "{acme_account.uri}"
|
||||
},
|
||||
"payload": {
|
||||
@@ -105,16 +105,16 @@ Feature: Order
|
||||
|
||||
Scenario Outline: Create an order with invalid identifier values
|
||||
Given I have an ACME cert profile as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
And I peak and memorize the next nonce as nonce
|
||||
When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order"
|
||||
When I send a raw ACME request to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order"
|
||||
"""
|
||||
{
|
||||
"protected": {
|
||||
"alg": "RS256",
|
||||
"nonce": "{nonce}",
|
||||
"url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order",
|
||||
"url": "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/new-order",
|
||||
"kid": "{acme_account.uri}"
|
||||
},
|
||||
"payload": {
|
||||
|
||||
@@ -56,7 +56,7 @@ def step_impl(context: Context, profile_var: str):
|
||||
profile_slug = faker.slug()
|
||||
jwt_token = context.vars["AUTH_TOKEN"]
|
||||
response = context.http_client.post(
|
||||
"/api/v1/pki/certificate-profiles",
|
||||
"/api/v1/cert-manager/certificate-profiles",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
json={
|
||||
"projectId": context.vars["PROJECT_ID"],
|
||||
@@ -74,7 +74,7 @@ def step_impl(context: Context, profile_var: str):
|
||||
kid = profile_id
|
||||
|
||||
response = context.http_client.get(
|
||||
f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
|
||||
f"/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
)
|
||||
response.raise_for_status()
|
||||
@@ -147,13 +147,47 @@ def step_impl(context: Context, var_name: str):
|
||||
context.vars[var_name] = response
|
||||
|
||||
|
||||
@given("I create a DNS Made Easy connection as {var_name}")
|
||||
def step_impl(context: Context, var_name: str):
|
||||
jwt_token = context.vars["AUTH_TOKEN"]
|
||||
conn_slug = faker.slug()
|
||||
with with_nocks(
|
||||
context,
|
||||
definitions=[
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "GET",
|
||||
"path": "/V2.0/dns/managed/",
|
||||
"status": 200,
|
||||
"response": {"totalRecords": 0, "totalPages": 1, "data": [], "page": 0},
|
||||
"responseIsBinary": False,
|
||||
}
|
||||
],
|
||||
):
|
||||
response = context.http_client.post(
|
||||
"/api/v1/app-connections/dns-made-easy",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
json={
|
||||
"name": conn_slug,
|
||||
"description": "",
|
||||
"method": "api-key-secret",
|
||||
"credentials": {
|
||||
"apiKey": "MOCK_API_KEY",
|
||||
"secretKey": "MOCK_SECRET_KEY",
|
||||
},
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
context.vars[var_name] = response
|
||||
|
||||
|
||||
@given("I create a external ACME CA with the following config as {var_name}")
|
||||
def step_impl(context: Context, var_name: str):
|
||||
jwt_token = context.vars["AUTH_TOKEN"]
|
||||
ca_slug = faker.slug()
|
||||
config = replace_vars(json.loads(context.text), context.vars)
|
||||
response = context.http_client.post(
|
||||
"/api/v1/pki/ca/acme",
|
||||
"/api/v1/cert-manager/ca/acme",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
json={
|
||||
"projectId": context.vars["PROJECT_ID"],
|
||||
@@ -174,7 +208,7 @@ def step_impl(context: Context, var_name: str):
|
||||
template_slug = faker.slug()
|
||||
config = replace_vars(json.loads(context.text), context.vars)
|
||||
response = context.http_client.post(
|
||||
"/api/v2/certificate-templates",
|
||||
"/api/v1/cert-manager/certificate-templates",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
json={
|
||||
"projectId": context.vars["PROJECT_ID"],
|
||||
@@ -194,7 +228,7 @@ def step_impl(context: Context, ca_id: str, template_id: str, profile_var: str):
|
||||
profile_slug = faker.slug()
|
||||
jwt_token = context.vars["AUTH_TOKEN"]
|
||||
response = context.http_client.post(
|
||||
"/api/v1/pki/certificate-profiles",
|
||||
"/api/v1/cert-manager/certificate-profiles",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
json={
|
||||
"projectId": context.vars["PROJECT_ID"],
|
||||
@@ -212,7 +246,7 @@ def step_impl(context: Context, ca_id: str, template_id: str, profile_var: str):
|
||||
kid = profile_id
|
||||
|
||||
response = context.http_client.get(
|
||||
f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
|
||||
f"/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
)
|
||||
response.raise_for_status()
|
||||
@@ -236,7 +270,7 @@ def step_impl(context: Context, profile_var: str):
|
||||
profile_slug = faker.slug()
|
||||
jwt_token = context.vars["AUTH_TOKEN"]
|
||||
response = context.http_client.post(
|
||||
"/api/v1/pki/certificate-profiles",
|
||||
"/api/v1/cert-manager/certificate-profiles",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
json={
|
||||
"projectId": context.vars["PROJECT_ID"],
|
||||
@@ -254,7 +288,7 @@ def step_impl(context: Context, profile_var: str):
|
||||
kid = profile_id
|
||||
|
||||
response = context.http_client.get(
|
||||
f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
|
||||
f"/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn(TableName.ScimToken, "expiryNotificationSent");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable(TableName.ScimToken, (t) => {
|
||||
t.boolean("expiryNotificationSent").defaultTo(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn(TableName.ScimToken, "expiryNotificationSent");
|
||||
if (hasCol) {
|
||||
await knex.schema.alterTable(TableName.ScimToken, (t) => {
|
||||
t.dropColumn("expiryNotificationSent");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ export const ScimTokensSchema = z.object({
|
||||
description: z.string(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
expiryNotificationSent: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TScimTokens = z.infer<typeof ScimTokensSchema>;
|
||||
|
||||
@@ -110,7 +110,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
|
||||
await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" });
|
||||
},
|
||||
{ prefix: "/pki" }
|
||||
{ prefix: "/cert-manager" }
|
||||
);
|
||||
|
||||
await server.register(
|
||||
|
||||
@@ -77,7 +77,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
||||
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)
|
||||
server.route({
|
||||
method: "GET",
|
||||
@@ -99,7 +100,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => server.services.pkiAcme.getAcmeDirectory(req.params.profileId)
|
||||
});
|
||||
|
||||
// HEAD /api/v1/pki/acme/profiles/<profile_id>/new-nonce
|
||||
// HEAD /api/v1/cert-manager/acme/profiles/<profile_id>/new-nonce
|
||||
// New Nonce (RFC 8555 Section 7.2)
|
||||
server.route({
|
||||
method: "HEAD",
|
||||
@@ -126,7 +127,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/v1/pki/acme/profiles/<profile_id>/new-account
|
||||
// POST /api/v1/cert-manager/acme/profiles/<profile_id>/new-account
|
||||
// New Account (RFC 8555 Section 7.3)
|
||||
server.route({
|
||||
method: "POST",
|
||||
@@ -163,7 +164,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/v1/pki/acme/profiles/<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)
|
||||
server.route({
|
||||
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)
|
||||
server.route({
|
||||
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)
|
||||
server.route({
|
||||
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)
|
||||
server.route({
|
||||
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)
|
||||
server.route({
|
||||
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)
|
||||
server.route({
|
||||
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)
|
||||
server.route({
|
||||
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)
|
||||
server.route({
|
||||
method: "POST",
|
||||
|
||||
@@ -72,7 +72,6 @@ const ProjectTemplateEnvironmentsSchema = z
|
||||
position: z.number().min(1)
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.superRefine((environments, ctx) => {
|
||||
if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
|
||||
@@ -198,7 +197,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
||||
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
|
||||
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
|
||||
type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type),
|
||||
environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional()
|
||||
environments: ProjectTemplateEnvironmentsSchema.nullish().describe(ProjectTemplates.CREATE.environments)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -243,7 +242,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
||||
.describe(ProjectTemplates.UPDATE.name),
|
||||
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),
|
||||
roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles),
|
||||
environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments)
|
||||
environments: ProjectTemplateEnvironmentsSchema.nullish().describe(ProjectTemplates.UPDATE.environments)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -158,6 +158,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
},
|
||||
data: {
|
||||
...req.body,
|
||||
name: req.body.slug,
|
||||
...req.body.type,
|
||||
permissions: req.body.permissions || undefined
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { applyJitter } from "@app/lib/delay";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
||||
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
||||
@@ -15,7 +23,12 @@ import { TDynamicSecretLeaseConfig } from "./dynamic-secret-lease-types";
|
||||
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
identityDAL: TIdentityDALFactory;
|
||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById" | "findOne">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
|
||||
@@ -23,18 +36,24 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||
|
||||
export type TDynamicSecretLeaseQueueServiceFactory = {
|
||||
pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise<void>;
|
||||
setLeaseRevocation: (leaseId: string, expiryAt: Date) => Promise<void>;
|
||||
setLeaseRevocation: (leaseId: string, dynamicSecretId: string, expiryAt: Date) => Promise<void>;
|
||||
unsetLeaseRevocation: (leaseId: string) => Promise<void>;
|
||||
queueFailedRevocation: (leaseId: string, dynamicSecretId: string) => Promise<void>;
|
||||
init: () => Promise<void>;
|
||||
};
|
||||
|
||||
const MAX_REVOCATION_RETRY_COUNT = 10;
|
||||
|
||||
export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
queueService,
|
||||
dynamicSecretDAL,
|
||||
dynamicSecretProviders,
|
||||
dynamicSecretLeaseDAL,
|
||||
kmsService,
|
||||
folderDAL
|
||||
folderDAL,
|
||||
projectMembershipDAL,
|
||||
projectDAL,
|
||||
smtpService
|
||||
}: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => {
|
||||
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
|
||||
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
|
||||
@@ -48,10 +67,10 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
);
|
||||
};
|
||||
|
||||
const setLeaseRevocation = async (leaseId: string, expiryAt: Date) => {
|
||||
const setLeaseRevocation = async (leaseId: string, dynamicSecretId: string, expiryAt: Date) => {
|
||||
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
|
||||
QueueJobs.DynamicSecretRevocation,
|
||||
{ leaseId },
|
||||
{ leaseId, dynamicSecretId },
|
||||
{
|
||||
id: leaseId,
|
||||
singletonKey: leaseId,
|
||||
@@ -68,10 +87,53 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
|
||||
};
|
||||
|
||||
const queueFailedRevocation = async (leaseId: string, dynamicSecretId: string) => {
|
||||
const appConfig = getConfig();
|
||||
|
||||
const retryDelaySeconds = appConfig.isDevelopmentMode ? 1 : Math.floor(applyJitter(3_600_000 * 4) / 1000); // retry every 4 hours with 20% +- jitter (convert ms to seconds for pgboss)
|
||||
|
||||
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
|
||||
QueueJobs.DynamicSecretRevocation,
|
||||
{ leaseId, isRetry: true, dynamicSecretId },
|
||||
{
|
||||
singletonKey: `${leaseId}-retry`, // avoid conflicts with scheduled revocation
|
||||
retryDelay: retryDelaySeconds,
|
||||
retryLimit: MAX_REVOCATION_RETRY_COUNT, // we dont want it to ever hit the limit, we want the expireInHours to take effect.
|
||||
expireInHours: 23 // if we set it to 24 hours, pgboss will complain that the expireIn is too high
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const $queueDynamicSecretLeaseRevocationFailedEmail = async (leaseId: string, dynamicSecretId: string) => {
|
||||
const appConfig = getConfig();
|
||||
|
||||
const delay = appConfig.isDevelopmentMode ? 1_000 * 60 : 1_000 * 60 * 15; // 1 minute in development, 15 minutes in production
|
||||
|
||||
await queueService.queue(
|
||||
QueueName.DynamicSecretLeaseRevocationFailedEmail,
|
||||
QueueJobs.DynamicSecretLeaseRevocationFailedEmail,
|
||||
{
|
||||
leaseId
|
||||
},
|
||||
{
|
||||
jobId: `dynamic-secret-lease-revocation-failed-email-${dynamicSecretId}`,
|
||||
delay,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 1000 * 60 // 1 minute
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const $dynamicSecretQueueJob = async (
|
||||
jobName: string,
|
||||
jobId: string,
|
||||
data: { leaseId: string } | { dynamicSecretCfgId: string }
|
||||
data: { leaseId: string; dynamicSecretId: string; isRetry?: boolean } | { dynamicSecretCfgId: string },
|
||||
retryCount?: number
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (jobName === QueueJobs.DynamicSecretRevocation) {
|
||||
@@ -79,7 +141,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
logger.info("Dynamic secret lease revocation started: ", leaseId, jobId);
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
||||
if (!dynamicSecretLease) {
|
||||
throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findById(dynamicSecretLease.dynamicSecret.folderId);
|
||||
if (!folder)
|
||||
@@ -150,7 +214,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
}
|
||||
logger.info("Finished dynamic secret job", jobId);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
logger.error(error, "Failed to delete dynamic secret");
|
||||
|
||||
if (jobName === QueueJobs.DynamicSecretPruning) {
|
||||
const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
|
||||
@@ -161,20 +225,97 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
}
|
||||
|
||||
if (jobName === QueueJobs.DynamicSecretRevocation) {
|
||||
const { leaseId } = data as { leaseId: string };
|
||||
const { leaseId, isRetry, dynamicSecretId } = data as {
|
||||
leaseId: string;
|
||||
isRetry?: boolean;
|
||||
dynamicSecretId: string;
|
||||
};
|
||||
await dynamicSecretLeaseDAL.updateById(leaseId, {
|
||||
status: DynamicSecretStatus.FailedDeletion,
|
||||
statusDetails: (error as Error)?.message?.slice(0, 255)
|
||||
statusDetails: `${(error as Error)?.message?.slice(0, 255)} - Retrying automatically`
|
||||
});
|
||||
|
||||
// only add to retry queue if this is not a retry, and if the error is not a DisableRotationErrors error
|
||||
if (!isRetry && !(error instanceof DisableRotationErrors)) {
|
||||
// if revocation fails, we should stop the job and queue a new job to retry the revocation at a later time.
|
||||
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
|
||||
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
|
||||
await queueFailedRevocation(leaseId, dynamicSecretId);
|
||||
|
||||
// if its the last attempt, and the error isn't a DisableRotationErrors error, send an email to the project admins (debounced)
|
||||
} else if (isRetry && !(error instanceof DisableRotationErrors)) {
|
||||
if (retryCount && retryCount === MAX_REVOCATION_RETRY_COUNT) {
|
||||
// if all retries fail, we should also stop the automatic revocation job.
|
||||
// the ID of the revocation job is set to the leaseId, so we can use that to stop the job
|
||||
|
||||
// we dont have to stop the retry job, because if we hit this point, its the last attempt and the retry job will be stopped by pgboss itself after this point,
|
||||
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
|
||||
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, leaseId);
|
||||
|
||||
await $queueDynamicSecretLeaseRevocationFailedEmail(leaseId, dynamicSecretId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (error instanceof DisableRotationErrors) {
|
||||
if (jobId) {
|
||||
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
|
||||
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
|
||||
}
|
||||
} else {
|
||||
// propagate to next part
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// send alert email once all revocation attempts have failed
|
||||
const $dynamicSecretLeaseRevocationFailedEmailJob = async (jobId: string, data: { leaseId: string }) => {
|
||||
try {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { leaseId } = data;
|
||||
logger.info(
|
||||
{ leaseId, jobId },
|
||||
"Dynamic secret revocation failed. Notifying project admins about failed revocation."
|
||||
);
|
||||
|
||||
const lease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!lease) {
|
||||
throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findById(lease.dynamicSecret.folderId);
|
||||
if (!folder) throw new NotFoundError({ message: `Failed to find folder with ${lease.dynamicSecret.folderId}` });
|
||||
|
||||
const project = await projectDAL.findById(folder.projectId);
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(project.id);
|
||||
|
||||
const projectAdmins = projectMembers.filter((member) =>
|
||||
member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
template: SmtpTemplates.DynamicSecretLeaseRevocationFailed,
|
||||
subjectLine: "Dynamic Secret Lease Revocation Failed",
|
||||
substitutions: {
|
||||
dynamicSecretLeaseUrl: `${appCfg.SITE_URL}/organizations/${project.orgId}/projects/secret-management/${project.id}/secrets/${folder.environment.envSlug}?dynamicSecretId=${lease.dynamicSecret.id}&filterBy=dynamic&search=${lease.dynamicSecret.name}`,
|
||||
dynamicSecretName: lease.dynamicSecret.name,
|
||||
projectName: project.name,
|
||||
environmentSlug: folder.environment.envSlug,
|
||||
errorMessage: lease.statusDetails || "An unknown error occurred"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to send dynamic secret lease revocation failed email");
|
||||
if (error instanceof DisableRotationErrors) {
|
||||
if (jobId) {
|
||||
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretLeaseRevocationFailedEmail, jobId);
|
||||
await queueService.stopJobById(QueueName.DynamicSecretLeaseRevocationFailedEmail, jobId);
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
// propogate to next part
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -182,14 +323,21 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
await $dynamicSecretQueueJob(job.name, job.id as string, job.data);
|
||||
});
|
||||
|
||||
// we use redis for sending the email because:
|
||||
// 1. we are insensitive to losing the jobs in queue in case of a disaster event
|
||||
// 2. pgboss does not support exclusive job keys on v0.10.x, and upgrading to v0.11.x which supports exclusive jobs comes with a lot of breaking changes, and we would need to manually migrate our existing jobs to the new version
|
||||
queueService.start(QueueName.DynamicSecretLeaseRevocationFailedEmail, async (job) => {
|
||||
await $dynamicSecretLeaseRevocationFailedEmailJob(job.id as string, job.data);
|
||||
});
|
||||
|
||||
const init = async () => {
|
||||
await queueService.startPg<QueueName.DynamicSecretRevocation>(
|
||||
QueueJobs.DynamicSecretRevocation,
|
||||
async ([job]) => {
|
||||
await $dynamicSecretQueueJob(job.name, job.id, job.data);
|
||||
await $dynamicSecretQueueJob(job.name, job.id, job.data, job.retryCount);
|
||||
},
|
||||
{
|
||||
workerCount: 5,
|
||||
workerCount: 10,
|
||||
pollingIntervalSeconds: 1
|
||||
}
|
||||
);
|
||||
@@ -210,6 +358,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
pruneDynamicSecret,
|
||||
setLeaseRevocation,
|
||||
unsetLeaseRevocation,
|
||||
queueFailedRevocation,
|
||||
init
|
||||
};
|
||||
};
|
||||
|
||||
@@ -178,7 +178,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
config
|
||||
});
|
||||
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, dynamicSecretCfg.id, expireAt);
|
||||
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
|
||||
};
|
||||
|
||||
@@ -272,7 +272,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
);
|
||||
|
||||
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
|
||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, dynamicSecretCfg.id, expireAt);
|
||||
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
||||
expireAt,
|
||||
externalEntityId: entityId
|
||||
@@ -358,11 +358,13 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
if ((revokeResponse as { error?: Error })?.error) {
|
||||
const { error } = revokeResponse as { error?: Error };
|
||||
logger.error(error?.message, "Failed to revoke lease");
|
||||
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
||||
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
|
||||
status: DynamicSecretLeaseStatus.FailedDeletion,
|
||||
statusDetails: error?.message?.slice(0, 255)
|
||||
});
|
||||
return deletedDynamicSecretLease;
|
||||
// queue a job to retry the revocation at a later time
|
||||
await dynamicSecretQueueService.queueFailedRevocation(dynamicSecretLease.id, dynamicSecretCfg.id);
|
||||
return updatedDynamicSecretLease;
|
||||
}
|
||||
|
||||
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AcmeAccountDoesNotExistError } from "./pki-acme-errors";
|
||||
export const buildUrl = (profileId: string, path: string): string => {
|
||||
const appCfg = getConfig();
|
||||
const baseUrl = appCfg.SITE_URL ?? "";
|
||||
return `${baseUrl}/api/v1/pki/acme/profiles/${profileId}${path}`;
|
||||
return `${baseUrl}/api/v1/cert-manager/acme/profiles/${profileId}${path}`;
|
||||
};
|
||||
|
||||
export const extractAccountIdFromKid = (kid: string, profileId: string): string => {
|
||||
|
||||
@@ -776,8 +776,9 @@ export const pkiAcmeServiceFactory = ({
|
||||
const cert = await orderCertificate(
|
||||
{
|
||||
caId: certificateAuthority!.id,
|
||||
profileId,
|
||||
commonName: certificateRequest.commonName!,
|
||||
// It is possible that the CSR does not have a common name, in which case we use an empty string
|
||||
// (more likely than not for a CSR from a modern ACME client like certbot, cert-manager, etc.)
|
||||
commonName: certificateRequest.commonName ?? "",
|
||||
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
|
||||
csr: Buffer.from(csrPem),
|
||||
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
|
||||
|
||||
@@ -189,11 +189,15 @@ export const projectTemplateServiceFactory = ({
|
||||
message: `A project template with the name "${params.name}" already exists.`
|
||||
});
|
||||
|
||||
const projectTemplateEnvironments =
|
||||
type === ProjectType.SecretManager && environments === undefined
|
||||
? ProjectTemplateDefaultEnvironments
|
||||
: environments;
|
||||
|
||||
const projectTemplate = await projectTemplateDAL.create({
|
||||
...params,
|
||||
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
|
||||
environments:
|
||||
type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null,
|
||||
environments: JSON.stringify(projectTemplateEnvironments),
|
||||
orgId: actor.orgId,
|
||||
type
|
||||
});
|
||||
|
||||
@@ -622,7 +622,7 @@ export const samlConfigServiceFactory = ({
|
||||
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
|
||||
newUser = await userDAL.create(
|
||||
{
|
||||
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
|
||||
username: serverCfg.trustSamlEmails ? email.toLowerCase() : uniqueUsername,
|
||||
email,
|
||||
isEmailVerified: serverCfg.trustSamlEmails,
|
||||
firstName,
|
||||
@@ -639,7 +639,7 @@ export const samlConfigServiceFactory = ({
|
||||
userId: newUser.id,
|
||||
aliasType: UserAliasType.SAML,
|
||||
externalId,
|
||||
emails: email ? [email] : [],
|
||||
emails: email ? [email.toLowerCase()] : [],
|
||||
orgId,
|
||||
isEmailVerified: serverCfg.trustSamlEmails
|
||||
},
|
||||
|
||||
@@ -1,10 +1,56 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, TOrmify } from "@app/lib/knex";
|
||||
import { AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TScimDALFactory = TOrmify<TableName.ScimToken>;
|
||||
import { TExpiringScimToken } from "./scim-types";
|
||||
|
||||
export const scimDALFactory = (db: TDbClient): TScimDALFactory => {
|
||||
export type TScimDALFactory = ReturnType<typeof scimDALFactory>;
|
||||
|
||||
export const scimDALFactory = (db: TDbClient) => {
|
||||
const scimTokenOrm = ormify(db, TableName.ScimToken);
|
||||
return scimTokenOrm;
|
||||
|
||||
const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0): Promise<TExpiringScimToken[]> => {
|
||||
try {
|
||||
const batch = await (tx || db.replicaNode())(TableName.ScimToken)
|
||||
.leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.ScimToken}.orgId`)
|
||||
.leftJoin(TableName.Membership, `${TableName.Membership}.scopeOrgId`, `${TableName.ScimToken}.orgId`)
|
||||
.leftJoin(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`)
|
||||
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`)
|
||||
.whereRaw(
|
||||
`
|
||||
(${TableName.ScimToken}."ttlDays" > 0 AND
|
||||
(${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") < NOW() + INTERVAL '7 days' AND
|
||||
(${TableName.ScimToken}."createdAt" + INTERVAL '1 day' * ${TableName.ScimToken}."ttlDays") > NOW())
|
||||
`
|
||||
)
|
||||
.where(`${TableName.ScimToken}.expiryNotificationSent`, false)
|
||||
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
|
||||
.where(`${TableName.MembershipRole}.role`, OrgMembershipRole.Admin)
|
||||
.whereNot(`${TableName.Membership}.status`, OrgMembershipStatus.Invited)
|
||||
.whereNotNull(`${TableName.Membership}.actorUserId`)
|
||||
.where(`${TableName.Users}.isGhost`, false)
|
||||
.whereNotNull(`${TableName.Users}.email`)
|
||||
.groupBy([`${TableName.ScimToken}.id`, `${TableName.Organization}.name`])
|
||||
.select<TExpiringScimToken[]>([
|
||||
db.ref("id").withSchema(TableName.ScimToken),
|
||||
db.ref("ttlDays").withSchema(TableName.ScimToken),
|
||||
db.ref("description").withSchema(TableName.ScimToken),
|
||||
db.ref("orgId").withSchema(TableName.ScimToken),
|
||||
db.ref("createdAt").withSchema(TableName.ScimToken),
|
||||
db.ref("name").withSchema(TableName.Organization).as("orgName"),
|
||||
db.raw(`array_agg(${TableName.Users}."email") as "adminEmails"`)
|
||||
])
|
||||
.limit(batchSize)
|
||||
.offset(offset);
|
||||
|
||||
return batch;
|
||||
} catch (err) {
|
||||
throw new DatabaseError({ error: err, name: "FindExpiringTokens" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...scimTokenOrm, findExpiringTokens };
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
@@ -47,7 +48,7 @@ import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, p
|
||||
import { TScimGroup, TScimServiceFactory } from "./scim-types";
|
||||
|
||||
type TScimServiceFactoryDep = {
|
||||
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
|
||||
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById" | "findExpiringTokens" | "update">;
|
||||
userDAL: Pick<
|
||||
TUserDALFactory,
|
||||
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
|
||||
@@ -389,15 +390,13 @@ export const scimServiceFactory = ({
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (trustScimEmails) {
|
||||
user = await userDAL.findOne(
|
||||
{
|
||||
email: email.toLowerCase(),
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
user = await userDAL.findOne(
|
||||
{
|
||||
email: email.toLowerCase(),
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
const uniqueUsername = await normalizeUsername(
|
||||
@@ -425,7 +424,8 @@ export const scimServiceFactory = ({
|
||||
aliasType,
|
||||
externalId,
|
||||
emails: email ? [email.toLowerCase()] : [],
|
||||
orgId
|
||||
orgId,
|
||||
isEmailVerified: trustScimEmails
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -1237,6 +1237,70 @@ export const scimServiceFactory = ({
|
||||
return { scimTokenId: scimToken.id, orgId: scimToken.orgId };
|
||||
};
|
||||
|
||||
const notifyExpiringTokens: TScimServiceFactory["notifyExpiringTokens"] = async () => {
|
||||
const appCfg = getConfig();
|
||||
let processedCount = 0;
|
||||
let hasMoreRecords = true;
|
||||
let offset = 0;
|
||||
const batchSize = 500;
|
||||
|
||||
while (hasMoreRecords) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const expiringTokens = await scimDAL.findExpiringTokens(undefined, batchSize, offset);
|
||||
|
||||
if (expiringTokens.length === 0) {
|
||||
hasMoreRecords = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const successfullyNotifiedTokenIds: string[] = [];
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(
|
||||
expiringTokens.map(async (token) => {
|
||||
try {
|
||||
if (token.adminEmails.length === 0) {
|
||||
// Still mark as notified to avoid repeated checks
|
||||
successfullyNotifiedTokenIds.push(token.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const createdOn = new Date(token.createdAt);
|
||||
const expiringOn = new Date(createdOn.getTime() + Number(token.ttlDays) * 86400 * 1000);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: token.adminEmails,
|
||||
subjectLine: "SCIM Token Expiry Notice",
|
||||
template: SmtpTemplates.ScimTokenExpired,
|
||||
substitutions: {
|
||||
tokenDescription: token.description,
|
||||
orgName: token.orgName,
|
||||
url: `${appCfg.SITE_URL}/organizations/${token.orgId}/settings?selectedTab=provisioning-settings`,
|
||||
createdOn,
|
||||
expiringOn
|
||||
}
|
||||
});
|
||||
|
||||
successfullyNotifiedTokenIds.push(token.id);
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to send expiration notification for SCIM token ${token.id}:`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Batch update all successfully notified tokens in a single query
|
||||
if (successfullyNotifiedTokenIds.length > 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await scimDAL.update({ $in: { id: successfullyNotifiedTokenIds } }, { expiryNotificationSent: true });
|
||||
}
|
||||
|
||||
processedCount += expiringTokens.length;
|
||||
offset += batchSize;
|
||||
}
|
||||
|
||||
return processedCount;
|
||||
};
|
||||
|
||||
return {
|
||||
createScimToken,
|
||||
listScimTokens,
|
||||
@@ -1253,6 +1317,7 @@ export const scimServiceFactory = ({
|
||||
deleteScimGroup,
|
||||
replaceScimGroup,
|
||||
updateScimGroup,
|
||||
fnValidateScimToken
|
||||
fnValidateScimToken,
|
||||
notifyExpiringTokens
|
||||
};
|
||||
};
|
||||
|
||||
@@ -158,6 +158,16 @@ export type TScimGroup = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TExpiringScimToken = {
|
||||
id: string;
|
||||
ttlDays: number;
|
||||
description: string;
|
||||
orgId: string;
|
||||
createdAt: Date;
|
||||
orgName: string;
|
||||
adminEmails: string[];
|
||||
};
|
||||
|
||||
export type TScimServiceFactory = {
|
||||
createScimToken: (arg: TCreateScimTokenDTO) => Promise<{
|
||||
scimToken: string;
|
||||
@@ -200,4 +210,5 @@ export type TScimServiceFactory = {
|
||||
scimTokenId: string;
|
||||
orgId: string;
|
||||
}>;
|
||||
notifyExpiringTokens: () => Promise<number>;
|
||||
};
|
||||
|
||||
@@ -1962,9 +1962,11 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
|
||||
export const CERTIFICATES = {
|
||||
GET: {
|
||||
id: "The ID of the certificate to get.",
|
||||
serialNumber: "The serial number of the certificate to get."
|
||||
},
|
||||
REVOKE: {
|
||||
id: "The ID of the certificate to revoke.",
|
||||
serialNumber:
|
||||
"The serial number of the certificate to revoke. The revoked certificate will be added to the certificate revocation list (CRL) of the CA.",
|
||||
revocationReason: "The reason for revoking the certificate.",
|
||||
@@ -1972,9 +1974,11 @@ export const CERTIFICATES = {
|
||||
serialNumberRes: "The serial number of the revoked certificate."
|
||||
},
|
||||
DELETE: {
|
||||
id: "The ID of the certificate to delete.",
|
||||
serialNumber: "The serial number of the certificate to delete."
|
||||
},
|
||||
GET_CERT: {
|
||||
id: "The ID of the certificate to get the certificate body and certificate chain for.",
|
||||
serialNumber: "The serial number of the certificate to get the certificate body and certificate chain for.",
|
||||
certificate: "The certificate body of the certificate.",
|
||||
certificateChain: "The certificate chain of the certificate.",
|
||||
|
||||
@@ -119,6 +119,7 @@ const envSchema = z
|
||||
})
|
||||
.default("{}")
|
||||
),
|
||||
DNS_MADE_EASY_SANDBOX_ENABLED: zodStrBool.default("false").optional(),
|
||||
// smtp options
|
||||
SMTP_HOST: zpStr(z.string().optional()),
|
||||
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
||||
|
||||
@@ -2,3 +2,13 @@ export const delay = (ms: number) =>
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
export const applyJitter = (delayMs: number) => {
|
||||
const jitterFactor = 0.2;
|
||||
|
||||
// generates random value in [-0.2, +0.2] range
|
||||
const randomFactor = (Math.random() * 2 - 1) * jitterFactor;
|
||||
const jitterAmount = randomFactor * delayMs;
|
||||
|
||||
return delayMs + jitterAmount;
|
||||
};
|
||||
|
||||
@@ -61,6 +61,7 @@ export enum QueueName {
|
||||
SecretPushEventScan = "secret-push-event-scan",
|
||||
UpgradeProjectToGhost = "upgrade-project-to-ghost",
|
||||
DynamicSecretRevocation = "dynamic-secret-revocation",
|
||||
DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email",
|
||||
CaCrlRotation = "ca-crl-rotation",
|
||||
CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber
|
||||
CertificateIssuance = "certificate-issuance",
|
||||
@@ -121,6 +122,7 @@ export enum QueueJobs {
|
||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||
CreateFolderTreeCheckpoint = "create-folder-tree-checkpoint",
|
||||
DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email",
|
||||
InvalidateCache = "invalidate-cache",
|
||||
SecretScanningV2FullScan = "secret-scanning-v2-full-scan",
|
||||
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan",
|
||||
@@ -221,11 +223,19 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.TelemetryInstanceStats;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.DynamicSecretLeaseRevocationFailedEmail]: {
|
||||
name: QueueJobs.DynamicSecretLeaseRevocationFailedEmail;
|
||||
payload: {
|
||||
leaseId: string;
|
||||
};
|
||||
};
|
||||
[QueueName.DynamicSecretRevocation]:
|
||||
| {
|
||||
name: QueueJobs.DynamicSecretRevocation;
|
||||
payload: {
|
||||
isRetry?: boolean;
|
||||
leaseId: string;
|
||||
dynamicSecretId: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DefaultResponseErrorsSchema } from "../routes/sanitizedSchemas";
|
||||
const isScimRoutes = (pathname: string) =>
|
||||
pathname.startsWith("/api/v1/scim/Users") || pathname.startsWith("/api/v1/scim/Groups");
|
||||
|
||||
const isAcmeRoutes = (pathname: string) => pathname.startsWith("/api/v1/pki/acme/");
|
||||
const isAcmeRoutes = (pathname: string) => pathname.startsWith("/api/v1/cert-manager/acme/");
|
||||
|
||||
export const addErrorsToResponseSchemas = fp(async (server) => {
|
||||
server.addHook("onRoute", (routeOptions) => {
|
||||
|
||||
@@ -43,7 +43,9 @@ export const registerServeUI = async (
|
||||
const frontendPath = path.join(dir, frontendName);
|
||||
await server.register(staticServe, {
|
||||
root: frontendPath,
|
||||
wildcard: false
|
||||
wildcard: false,
|
||||
maxAge: "30d",
|
||||
immutable: true
|
||||
});
|
||||
|
||||
server.route({
|
||||
@@ -58,11 +60,12 @@ export const registerServeUI = async (
|
||||
return;
|
||||
}
|
||||
|
||||
// This should help avoid caching any chunks (temp fix)
|
||||
void reply.header("Cache-Control", "no-cache, no-store, must-revalidate, private, max-age=0");
|
||||
void reply.header("Pragma", "no-cache");
|
||||
void reply.header("Expires", "0");
|
||||
return reply.sendFile("index.html");
|
||||
return reply.sendFile("index.html", {
|
||||
immutable: false,
|
||||
maxAge: 0,
|
||||
lastModified: false,
|
||||
etag: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1333,7 +1333,8 @@ export const registerRoutes = async (
|
||||
eventBusService,
|
||||
licenseService,
|
||||
membershipRoleDAL,
|
||||
membershipUserDAL
|
||||
membershipUserDAL,
|
||||
telemetryService
|
||||
});
|
||||
|
||||
const projectService = projectServiceFactory({
|
||||
@@ -1878,7 +1879,12 @@ export const registerRoutes = async (
|
||||
dynamicSecretProviders,
|
||||
dynamicSecretDAL,
|
||||
folderDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
smtpService,
|
||||
userDAL,
|
||||
identityDAL,
|
||||
projectMembershipDAL,
|
||||
projectDAL
|
||||
});
|
||||
const dynamicSecretService = dynamicSecretServiceFactory({
|
||||
projectDAL,
|
||||
@@ -1911,6 +1917,7 @@ export const registerRoutes = async (
|
||||
|
||||
// DAILY
|
||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||
scimService,
|
||||
auditLogDAL,
|
||||
queueService,
|
||||
secretVersionDAL,
|
||||
|
||||
@@ -61,6 +61,10 @@ import {
|
||||
DigitalOceanConnectionListItemSchema,
|
||||
SanitizedDigitalOceanConnectionSchema
|
||||
} from "@app/services/app-connection/digital-ocean";
|
||||
import {
|
||||
DNSMadeEasyConnectionListItemSchema,
|
||||
SanitizedDNSMadeEasyConnectionSchema
|
||||
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
|
||||
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||
@@ -170,7 +174,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedAzureADCSConnectionSchema.options,
|
||||
...SanitizedRedisConnectionSchema.options,
|
||||
...SanitizedLaravelForgeConnectionSchema.options,
|
||||
...SanitizedChefConnectionSchema.options
|
||||
...SanitizedChefConnectionSchema.options,
|
||||
...SanitizedDNSMadeEasyConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -215,7 +220,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
AzureADCSConnectionListItemSchema,
|
||||
RedisConnectionListItemSchema,
|
||||
LaravelForgeConnectionListItemSchema,
|
||||
ChefConnectionListItemSchema
|
||||
ChefConnectionListItemSchema,
|
||||
DNSMadeEasyConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateDNSMadeEasyConnectionSchema,
|
||||
SanitizedDNSMadeEasyConnectionSchema,
|
||||
UpdateDNSMadeEasyConnectionSchema
|
||||
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerDNSMadeEasyConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.DNSMadeEasy,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedDNSMadeEasyConnectionSchema,
|
||||
createSchema: CreateDNSMadeEasyConnectionSchema,
|
||||
updateSchema: UpdateDNSMadeEasyConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/dns-made-easy-zones`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const zones = await server.services.appConnection.dnsMadeEasy.listZones(connectionId, req.permission);
|
||||
return zones;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
|
||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
||||
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
@@ -78,6 +79,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Flyio]: registerFlyioConnectionRouter,
|
||||
[AppConnection.GitLab]: registerGitLabConnectionRouter,
|
||||
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
|
||||
[AppConnection.DNSMadeEasy]: registerDNSMadeEasyConnectionRouter,
|
||||
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
|
||||
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
|
||||
[AppConnection.Railway]: registerRailwayConnectionRouter,
|
||||
|
||||
@@ -85,7 +85,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
isInternal: false,
|
||||
actorOrgId: req.permission.orgId,
|
||||
enableDirectIssuance: !req.body.requireTemplateForIssuance,
|
||||
...req.body
|
||||
});
|
||||
|
||||
@@ -220,7 +219,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
isInternal: false,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
enableDirectIssuance: !req.body.requireTemplateForIssuance,
|
||||
...req.body
|
||||
});
|
||||
|
||||
@@ -617,6 +615,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: DEPRECATE
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:caId/issue-certificate",
|
||||
@@ -625,7 +624,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Issue certificate from CA",
|
||||
params: z.object({
|
||||
@@ -711,6 +709,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: DEPRECATE
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:caId/sign-certificate",
|
||||
@@ -719,7 +718,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Sign certificate from CA",
|
||||
params: z.object({
|
||||
@@ -805,6 +803,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: DEPRECATE
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/certificate-templates",
|
||||
@@ -813,7 +812,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Get list of certificate templates for the CA",
|
||||
params: z.object({
|
||||
@@ -854,6 +852,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: DEPRECATE
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/crls",
|
||||
@@ -862,7 +861,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Get list of CRLs of the CA",
|
||||
params: z.object({
|
||||
|
||||
@@ -28,14 +28,10 @@ export const registerCertificateAuthorityEndpoints = <
|
||||
projectId: string;
|
||||
status: CaStatus;
|
||||
configuration: I["configuration"];
|
||||
enableDirectIssuance: boolean;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
projectId: string;
|
||||
name?: string;
|
||||
status?: CaStatus;
|
||||
configuration?: I["configuration"];
|
||||
enableDirectIssuance?: boolean;
|
||||
}>;
|
||||
responseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
@@ -83,7 +79,7 @@ export const registerCertificateAuthorityEndpoints = <
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caName",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
@@ -91,10 +87,7 @@ export const registerCertificateAuthorityEndpoints = <
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
params: z.object({
|
||||
caName: z.string()
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string().uuid()
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: responseSchema
|
||||
@@ -102,14 +95,12 @@ export const registerCertificateAuthorityEndpoints = <
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { caName } = req.params;
|
||||
const { projectId } = req.query;
|
||||
const { id } = req.params;
|
||||
|
||||
const certificateAuthority =
|
||||
(await server.services.certificateAuthority.findCertificateAuthorityByNameAndProjectId(
|
||||
{ caName, type: caType, projectId },
|
||||
req.permission
|
||||
)) as T;
|
||||
const certificateAuthority = (await server.services.certificateAuthority.findCertificateAuthorityById(
|
||||
{ id, type: caType },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
@@ -166,7 +157,7 @@ export const registerCertificateAuthorityEndpoints = <
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:caName",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
@@ -174,7 +165,7 @@ export const registerCertificateAuthorityEndpoints = <
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
params: z.object({
|
||||
caName: z.string()
|
||||
id: z.string()
|
||||
}),
|
||||
body: updateSchema,
|
||||
response: {
|
||||
@@ -183,13 +174,13 @@ export const registerCertificateAuthorityEndpoints = <
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { caName } = req.params;
|
||||
const { id } = req.params;
|
||||
|
||||
const certificateAuthority = (await server.services.certificateAuthority.updateCertificateAuthority(
|
||||
{
|
||||
...req.body,
|
||||
type: caType,
|
||||
caName
|
||||
id
|
||||
},
|
||||
req.permission
|
||||
)) as T;
|
||||
@@ -213,7 +204,7 @@ export const registerCertificateAuthorityEndpoints = <
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:caName",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
@@ -221,10 +212,7 @@ export const registerCertificateAuthorityEndpoints = <
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
params: z.object({
|
||||
caName: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
projectId: z.string().uuid()
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: responseSchema
|
||||
@@ -232,11 +220,10 @@ export const registerCertificateAuthorityEndpoints = <
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { caName } = req.params;
|
||||
const { projectId } = req.body;
|
||||
const { id } = req.params;
|
||||
|
||||
const certificateAuthority = (await server.services.certificateAuthority.deleteCertificateAuthority(
|
||||
{ caName, type: caType, projectId },
|
||||
{ id, type: caType },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
CreateInternalCertificateAuthoritySchema,
|
||||
InternalCertificateAuthoritySchema,
|
||||
@@ -15,4 +23,406 @@ export const registerInternalCertificateAuthorityRouter = async (server: Fastify
|
||||
createSchema: CreateInternalCertificateAuthoritySchema,
|
||||
updateSchema: UpdateInternalCertificateAuthoritySchema
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/csr",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Get CA CSR",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CSR.caId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
csr: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CSR.csr)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { ca, csr } = await server.services.internalCertificateAuthority.getCaCsr({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CA_CSR,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
csr
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:caId/renew",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Perform CA certificate renewal",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.caId)
|
||||
}),
|
||||
body: z.object({
|
||||
type: z.nativeEnum(CaRenewalType).describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.type),
|
||||
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.notAfter)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.certificateChain),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.RENEW_CA_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, serialNumber, ca } =
|
||||
await server.services.internalCertificateAuthority.renewCaCert({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.RENEW_CA,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/ca-certificates",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Get list of past and current CA certificates for a CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.caId)
|
||||
}),
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificate),
|
||||
certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.certificateChain),
|
||||
serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.serialNumber),
|
||||
version: z.number().describe(CERTIFICATE_AUTHORITIES.GET_CA_CERTS.version)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { caCerts, ca } = await server.services.internalCertificateAuthority.getCaCerts({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CA_CERTS,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return caCerts;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/certificate",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Get current CA cert and cert chain of a CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT.caId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificate),
|
||||
certificateChain: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.certificateChain),
|
||||
serialNumber: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, serialNumber, ca } =
|
||||
await server.services.internalCertificateAuthority.getCaCert({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CA_CERT,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:caId/sign-intermediate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Create intermediate CA certificate from parent CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
|
||||
}),
|
||||
body: z.object({
|
||||
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
|
||||
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
|
||||
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.certificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.certificateChain),
|
||||
issuingCaCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.issuingCaCertificate),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.internalCertificateAuthority.signIntermediate({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.SIGN_INTERMEDIATE,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:caId/import-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Import certificate and chain to CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.caId)
|
||||
}),
|
||||
body: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.certificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.IMPORT_CERT.certificateChain)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string().trim(),
|
||||
caId: z.string().trim()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { ca } = await server.services.internalCertificateAuthority.importCertToCa({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.IMPORT_CA_CERT,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Successfully imported certificate to CA",
|
||||
caId: req.params.caId
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/crls",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Get list of CRLs of the CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.caId)
|
||||
}),
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
id: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.id),
|
||||
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.crl)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { ca, crls } = await server.services.certificateAuthorityCrl.getCaCrls({
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CA_CRLS,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return crls;
|
||||
}
|
||||
});
|
||||
|
||||
// this endpoint will be used to serve the CA certificate when a client makes a request
|
||||
// against the Authority Information Access CA Issuer URL
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/certificates/:caCertId/der",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
description: "Get DER-encoded certificate of CA",
|
||||
params: z.object({
|
||||
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caId),
|
||||
caCertId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caCertId)
|
||||
}),
|
||||
response: {
|
||||
200: z.instanceof(Buffer)
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const caCert = await server.services.internalCertificateAuthority.getCaCertById(req.params);
|
||||
|
||||
void res.header("Content-Type", "application/pkix-cert");
|
||||
|
||||
return Buffer.from(caCert.rawData);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,24 +4,510 @@ import { z } from "zod";
|
||||
|
||||
import { CertificatesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { ApiDocsTags, CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { addNoCacheHeaders } from "@app/server/lib/caching";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
ACMESANType,
|
||||
CertificateOrderStatus,
|
||||
CertKeyAlgorithm,
|
||||
CertSignatureAlgorithm,
|
||||
CrlReason
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import {
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
CertSubjectAlternativeNameType
|
||||
} from "@app/services/certificate-common/certificate-constants";
|
||||
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
|
||||
import { booleanSchema } from "../sanitizedSchemas";
|
||||
|
||||
interface CertificateRequestForService {
|
||||
commonName?: string;
|
||||
keyUsages?: CertKeyUsageType[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||
altNames?: Array<{
|
||||
type: CertSubjectAlternativeNameType;
|
||||
value: string;
|
||||
}>;
|
||||
validity: {
|
||||
ttl: string;
|
||||
};
|
||||
notBefore?: Date;
|
||||
notAfter?: Date;
|
||||
signatureAlgorithm?: string;
|
||||
keyAlgorithm?: string;
|
||||
}
|
||||
|
||||
const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => {
|
||||
const hasDateFields = data.notBefore || data.notAfter;
|
||||
const hasTtl = data.ttl;
|
||||
return !(hasDateFields && hasTtl);
|
||||
};
|
||||
|
||||
const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
|
||||
if (data.notBefore && data.notAfter) {
|
||||
const notBefore = new Date(data.notBefore);
|
||||
const notAfter = new Date(data.notAfter);
|
||||
return notBefore < notAfter;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/issue-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
body: z
|
||||
.object({
|
||||
profileId: z.string().uuid(),
|
||||
commonName: validateTemplateRegexField.optional(),
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "TTL cannot be empty")
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField.optional(),
|
||||
altNames: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||
value: z.string().min(1, "SAN value cannot be empty")
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
|
||||
removeRootsFromChain: booleanSchema.default(false).optional()
|
||||
})
|
||||
.refine(validateTtlAndDateFields, {
|
||||
message:
|
||||
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
|
||||
})
|
||||
.refine(validateDateOrder, {
|
||||
message: "notBefore must be earlier than notAfter"
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim(),
|
||||
issuingCaCertificate: z.string().trim(),
|
||||
certificateChain: z.string().trim(),
|
||||
privateKey: z.string().trim().optional(),
|
||||
serialNumber: z.string().trim(),
|
||||
certificateId: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificateRequestForService: CertificateRequestForService = {
|
||||
commonName: req.body.commonName,
|
||||
keyUsages: req.body.keyUsages,
|
||||
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||
altNames: req.body.altNames,
|
||||
validity: {
|
||||
ttl: req.body.ttl
|
||||
},
|
||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||
signatureAlgorithm: req.body.signatureAlgorithm,
|
||||
keyAlgorithm: req.body.keyAlgorithm
|
||||
};
|
||||
|
||||
const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService);
|
||||
|
||||
const data = await server.services.certificateV3.issueCertificateFromProfile({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: req.body.profileId,
|
||||
certificateRequest: mappedCertificateRequest,
|
||||
removeRootsFromChain: req.body.removeRootsFromChain
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE,
|
||||
metadata: {
|
||||
certificateProfileId: req.body.profileId,
|
||||
certificateId: data.certificateId,
|
||||
commonName: req.body.commonName || "",
|
||||
profileName: data.profileName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/sign-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
body: z
|
||||
.object({
|
||||
profileId: z.string().uuid(),
|
||||
csr: z.string().trim().min(1, "CSR cannot be empty").max(4096, "CSR cannot exceed 4096 characters"),
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "TTL cannot be empty")
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField.optional(),
|
||||
removeRootsFromChain: booleanSchema.default(false).optional()
|
||||
})
|
||||
.refine(validateTtlAndDateFields, {
|
||||
message:
|
||||
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
|
||||
})
|
||||
.refine(validateDateOrder, {
|
||||
message: "notBefore must be earlier than notAfter"
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim(),
|
||||
issuingCaCertificate: z.string().trim(),
|
||||
certificateChain: z.string().trim(),
|
||||
serialNumber: z.string().trim(),
|
||||
certificateId: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificateRequest = extractCertificateRequestFromCSR(req.body.csr);
|
||||
|
||||
const data = await server.services.certificateV3.signCertificateFromProfile({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: req.body.profileId,
|
||||
csr: req.body.csr,
|
||||
validity: {
|
||||
ttl: req.body.ttl
|
||||
},
|
||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||
enrollmentType: EnrollmentType.API,
|
||||
removeRootsFromChain: req.body.removeRootsFromChain
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE,
|
||||
metadata: {
|
||||
certificateProfileId: req.body.profileId,
|
||||
certificateId: data.certificateId,
|
||||
profileName: data.profileName,
|
||||
commonName: certificateRequest.commonName || ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/order-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
body: z
|
||||
.object({
|
||||
profileId: z.string().uuid(),
|
||||
subjectAlternativeNames: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "SAN value cannot be empty")
|
||||
.max(255, "SAN value must be less than 255 characters")
|
||||
})
|
||||
)
|
||||
.min(1, "At least one subject alternative name must be provided"),
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "TTL cannot be empty")
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField.optional(),
|
||||
commonName: validateTemplateRegexField.optional(),
|
||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
|
||||
removeRootsFromChain: booleanSchema.default(false).optional()
|
||||
})
|
||||
.refine(validateTtlAndDateFields, {
|
||||
message:
|
||||
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
|
||||
})
|
||||
.refine(validateDateOrder, {
|
||||
message: "notBefore must be earlier than notAfter"
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
orderId: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
subjectAlternativeNames: z.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus)
|
||||
})
|
||||
),
|
||||
authorizations: z.array(
|
||||
z.object({
|
||||
identifier: z.object({
|
||||
type: z.nativeEnum(ACMESANType),
|
||||
value: z.string()
|
||||
}),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
expires: z.string().optional(),
|
||||
challenges: z.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
status: z.nativeEnum(CertificateOrderStatus),
|
||||
url: z.string(),
|
||||
token: z.string()
|
||||
})
|
||||
)
|
||||
})
|
||||
),
|
||||
finalize: z.string(),
|
||||
certificate: z.string().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const data = await server.services.certificateV3.orderCertificateFromProfile({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: req.body.profileId,
|
||||
certificateOrder: {
|
||||
altNames: req.body.subjectAlternativeNames,
|
||||
validity: {
|
||||
ttl: req.body.ttl
|
||||
},
|
||||
commonName: req.body.commonName,
|
||||
keyUsages: req.body.keyUsages,
|
||||
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||
signatureAlgorithm: req.body.signatureAlgorithm,
|
||||
keyAlgorithm: req.body.keyAlgorithm
|
||||
},
|
||||
removeRootsFromChain: req.body.removeRootsFromChain
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
||||
metadata: {
|
||||
certificateProfileId: req.body.profileId,
|
||||
orderId: data.orderId,
|
||||
profileName: data.profileName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:id/renew",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
params: z.object({
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
removeRootsFromChain: booleanSchema.default(false).optional()
|
||||
})
|
||||
.optional(),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim(),
|
||||
issuingCaCertificate: z.string().trim(),
|
||||
certificateChain: z.string().trim(),
|
||||
privateKey: z.string().trim().optional(),
|
||||
serialNumber: z.string().trim(),
|
||||
certificateId: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const data = await server.services.certificateV3.renewCertificate({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
certificateId: req.params.id,
|
||||
removeRootsFromChain: req.body?.removeRootsFromChain
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.RENEW_CERTIFICATE,
|
||||
metadata: {
|
||||
originalCertificateId: req.params.id,
|
||||
newCertificateId: data.certificateId,
|
||||
profileName: data.profileName,
|
||||
commonName: data.commonName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id/config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
params: z.object({
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
renewBeforeDays: z.number().int().min(1).max(30).optional(),
|
||||
enableAutoRenewal: z.boolean().optional()
|
||||
})
|
||||
.refine((data) => !(data.renewBeforeDays !== undefined && data.enableAutoRenewal === false), {
|
||||
message: "Cannot specify both renewBeforeDays and enableAutoRenewal=false"
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
renewBeforeDays: z.number().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
if (req.body.enableAutoRenewal === false) {
|
||||
const data = await server.services.certificateV3.disableRenewalConfig({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
certificateId: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG,
|
||||
metadata: {
|
||||
certificateId: req.params.id,
|
||||
commonName: data.commonName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Auto-renewal disabled successfully"
|
||||
};
|
||||
}
|
||||
|
||||
if (req.body.renewBeforeDays !== undefined) {
|
||||
const data = await server.services.certificateV3.updateRenewalConfig({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
certificateId: req.params.id,
|
||||
renewBeforeDays: req.body.renewBeforeDays
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG,
|
||||
metadata: {
|
||||
certificateId: req.params.id,
|
||||
renewBeforeDays: req.body.renewBeforeDays.toString(),
|
||||
commonName: data.commonName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Certificate configuration updated successfully",
|
||||
renewBeforeDays: data.renewBeforeDays
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: "No configuration changes requested"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:serialNumber",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
@@ -31,7 +517,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Get certificate",
|
||||
params: z.object({
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
|
||||
id: z.string().trim().describe(CERTIFICATES.GET.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -41,7 +527,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { cert } = await server.services.certificate.getCert({
|
||||
serialNumber: req.params.serialNumber,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -67,10 +553,9 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:serialNumber/private-key",
|
||||
url: "/:id/private-key",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
@@ -80,7 +565,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Get certificate private key",
|
||||
params: z.object({
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
|
||||
id: z.string().trim().describe(CERTIFICATES.GET.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.string().trim()
|
||||
@@ -88,7 +573,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req, reply) => {
|
||||
const { cert, certPrivateKey } = await server.services.certificate.getCertPrivateKey({
|
||||
serialNumber: req.params.serialNumber,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -114,10 +599,9 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: In the future add support for other formats outside of PEM (such as DER). Adding a "format" query param may be best.
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:serialNumber/bundle",
|
||||
url: "/:id/bundle",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
@@ -127,7 +611,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Get certificate bundle including the certificate, chain, and private key.",
|
||||
params: z.object({
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber)
|
||||
id: z.string().trim().describe(CERTIFICATES.GET_CERT.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -141,7 +625,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req, reply) => {
|
||||
const { certificate, certificateChain, serialNumber, cert, privateKey } =
|
||||
await server.services.certificate.getCertBundle({
|
||||
serialNumber: req.params.serialNumber,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -172,120 +656,6 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/issue-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Issue certificate",
|
||||
body: z
|
||||
.object({
|
||||
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId),
|
||||
certificateTemplateId: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
|
||||
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
|
||||
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
|
||||
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
|
||||
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter),
|
||||
keyUsages: z
|
||||
.nativeEnum(CertKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.keyUsages),
|
||||
extendedKeyUsages: z
|
||||
.nativeEnum(CertExtendedKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.extendedKeyUsages)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const { ttl, notAfter } = data;
|
||||
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
|
||||
},
|
||||
{
|
||||
message: "Either ttl or notAfter must be present, but not both",
|
||||
path: ["ttl", "notAfter"]
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
|
||||
(data.caId === undefined && data.certificateTemplateId !== undefined),
|
||||
{
|
||||
message: "Either CA ID or Certificate Template ID must be present, but not both",
|
||||
path: ["caId", "certificateTemplateId"]
|
||||
}
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate),
|
||||
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
|
||||
privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
|
||||
await server.services.internalCertificateAuthority.issueCertFromCa({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.ISSUE_CERT,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
caId: req.body.caId,
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
commonName: req.body.commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
privateKey,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/import-certificate",
|
||||
@@ -350,121 +720,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/sign-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Sign certificate",
|
||||
body: z
|
||||
.object({
|
||||
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId),
|
||||
certificateTemplateId: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
|
||||
pkiCollectionId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.pkiCollectionId),
|
||||
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
|
||||
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
|
||||
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
|
||||
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
|
||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter),
|
||||
keyUsages: z
|
||||
.nativeEnum(CertKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.keyUsages),
|
||||
extendedKeyUsages: z
|
||||
.nativeEnum(CertExtendedKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.extendedKeyUsages)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const { ttl, notAfter } = data;
|
||||
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
|
||||
},
|
||||
{
|
||||
message: "Either ttl or notAfter must be present, but not both",
|
||||
path: ["ttl", "notAfter"]
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) =>
|
||||
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
|
||||
(data.caId === undefined && data.certificateTemplateId !== undefined),
|
||||
{
|
||||
message: "Either CA ID or Certificate Template ID must be present, but not both",
|
||||
path: ["caId", "certificateTemplateId"]
|
||||
}
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate),
|
||||
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
|
||||
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } =
|
||||
await server.services.internalCertificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: ca.projectId,
|
||||
event: {
|
||||
type: EventType.SIGN_CERT,
|
||||
metadata: {
|
||||
caId: ca.id,
|
||||
dn: ca.dn,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
caId: req.body.caId,
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:serialNumber/revoke",
|
||||
url: "/:id/revoke",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
@@ -474,7 +730,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Revoke",
|
||||
params: z.object({
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.REVOKE.serialNumber)
|
||||
id: z.string().trim().describe(CERTIFICATES.REVOKE.id)
|
||||
}),
|
||||
body: z.object({
|
||||
revocationReason: z.nativeEnum(CrlReason).describe(CERTIFICATES.REVOKE.revocationReason)
|
||||
@@ -489,7 +745,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { revokedAt, cert, ca } = await server.services.certificate.revokeCert({
|
||||
serialNumber: req.params.serialNumber,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -512,7 +768,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
return {
|
||||
message: "Successfully revoked certificate",
|
||||
serialNumber: req.params.serialNumber,
|
||||
serialNumber: cert.serialNumber,
|
||||
revokedAt
|
||||
};
|
||||
}
|
||||
@@ -520,7 +776,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:serialNumber",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
@@ -530,7 +786,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Delete certificate",
|
||||
params: z.object({
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.DELETE.serialNumber)
|
||||
id: z.string().trim().describe(CERTIFICATES.DELETE.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -540,7 +796,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { deletedCert } = await server.services.certificate.deleteCert({
|
||||
serialNumber: req.params.serialNumber,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -568,7 +824,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:serialNumber/certificate",
|
||||
url: "/:id/certificate",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
@@ -578,7 +834,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Get certificate body of certificate",
|
||||
params: z.object({
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumber)
|
||||
id: z.string().trim().describe(CERTIFICATES.GET_CERT.id)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -590,7 +846,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, serialNumber, cert } = await server.services.certificate.getCertBody({
|
||||
serialNumber: req.params.serialNumber,
|
||||
id: req.params.id,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -620,7 +876,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:serialNumber/pkcs12",
|
||||
url: "/:id/pkcs12",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
@@ -630,7 +886,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
description: "Download certificate in PKCS12 format",
|
||||
params: z.object({
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
|
||||
id: z.string().trim().describe(CERTIFICATES.GET.id)
|
||||
}),
|
||||
body: z.object({
|
||||
password: z
|
||||
@@ -645,7 +901,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
handler: async (req, reply) => {
|
||||
const { pkcs12Data, cert } = await server.services.certificate.getCertPkcs12({
|
||||
serialNumber: req.params.serialNumber,
|
||||
id: req.params.id,
|
||||
password: req.body.password,
|
||||
alias: req.body.alias,
|
||||
actor: req.permission.type,
|
||||
@@ -671,7 +927,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
reply.header("Content-Type", "application/octet-stream");
|
||||
reply.header(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="certificate-${req.params.serialNumber.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"`
|
||||
`attachment; filename="certificate-${cert.serialNumber?.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"`
|
||||
);
|
||||
|
||||
return pkcs12Data;
|
||||
|
||||
@@ -1,28 +1,239 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
import {
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
CertSubjectAlternativeNameType,
|
||||
CertSubjectAttributeType
|
||||
} from "@app/services/certificate-common/certificate-constants";
|
||||
import { certificateTemplateV2ResponseSchema } from "@app/services/certificate-template-v2/certificate-template-v2-schemas";
|
||||
|
||||
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
|
||||
id: true,
|
||||
certificateTemplateId: true,
|
||||
isEnabled: true,
|
||||
disableBootstrapCertValidation: true
|
||||
const attributeTypeSchema = z.nativeEnum(CertSubjectAttributeType);
|
||||
const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
|
||||
|
||||
const templateV2SubjectSchema = z
|
||||
.object({
|
||||
type: attributeTypeSchema,
|
||||
allowed: z.array(z.string()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
denied: z.array(z.string()).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Subject attribute must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
const templateV2KeyUsagesSchema = z
|
||||
.object({
|
||||
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Key usages must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
const templateV2ExtendedKeyUsagesSchema = z
|
||||
.object({
|
||||
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Extended key usages must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
const templateV2SanSchema = z
|
||||
.object({
|
||||
type: sanTypeSchema,
|
||||
allowed: z.array(z.string()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
denied: z.array(z.string()).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "SAN must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
const templateV2ValiditySchema = z.object({
|
||||
max: z
|
||||
.string()
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
if (val.length < 2) return false;
|
||||
const unit = val.slice(-1);
|
||||
const number = val.slice(0, -1);
|
||||
const digitRegex = new RE2("^\\d+$");
|
||||
return ["d", "h", "m", "y"].includes(unit) && digitRegex.test(number);
|
||||
},
|
||||
{
|
||||
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'"
|
||||
}
|
||||
)
|
||||
.optional()
|
||||
});
|
||||
|
||||
const templateV2AlgorithmsSchema = z.object({
|
||||
signature: z.array(z.string()).min(1, "At least one signature algorithm must be provided").optional(),
|
||||
keyAlgorithm: z.array(z.string()).min(1, "At least one key algorithm must be provided").optional()
|
||||
});
|
||||
|
||||
const createCertificateTemplateV2Schema = z.object({
|
||||
projectId: z.string().min(1),
|
||||
name: z.string().min(1).max(255, "Name must be between 1 and 255 characters"),
|
||||
description: z.string().max(1000).optional(),
|
||||
subject: z.array(templateV2SubjectSchema).optional(),
|
||||
sans: z.array(templateV2SanSchema).optional(),
|
||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
|
||||
algorithms: templateV2AlgorithmsSchema.optional(),
|
||||
validity: templateV2ValiditySchema.optional()
|
||||
});
|
||||
|
||||
const updateCertificateTemplateV2Schema = z.object({
|
||||
name: z.string().min(1).max(255, "Name must be between 1 and 255 characters").optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
subject: z.array(templateV2SubjectSchema).optional(),
|
||||
sans: z.array(templateV2SanSchema).optional(),
|
||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
|
||||
algorithms: templateV2AlgorithmsSchema.optional(),
|
||||
validity: templateV2ValiditySchema.optional()
|
||||
});
|
||||
|
||||
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||
body: createCertificateTemplateV2Schema,
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateTemplate: certificateTemplateV2ResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { projectId, ...data } = req.body;
|
||||
const certificateTemplate = await server.services.certificateTemplateV2.createTemplateV2({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod!,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
data
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
name: certificateTemplate.name,
|
||||
projectId: certificateTemplate.projectId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { certificateTemplate };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certificateTemplateId",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||
querystring: z.object({
|
||||
projectId: z.string().min(1),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
search: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateTemplates: certificateTemplateV2ResponseSchema.array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { templates, totalCount } = await server.services.certificateTemplateV2.listTemplatesV2({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod!,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.LIST_CERTIFICATE_TEMPLATES,
|
||||
metadata: {
|
||||
projectId: req.query.projectId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { certificateTemplates: templates, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
@@ -30,20 +241,22 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.GET.certificateTemplateId)
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedCertificateTemplate
|
||||
200: z.object({
|
||||
certificateTemplate: certificateTemplateV2ResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificateTemplate = await server.services.certificateTemplate.getCertTemplate({
|
||||
id: req.params.certificateTemplateId,
|
||||
const certificateTemplate = await server.services.certificateTemplateV2.getTemplateV2ById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
actorAuthMethod: req.permission.authMethod!,
|
||||
actorOrgId: req.permission.orgId,
|
||||
templateId: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@@ -58,125 +271,38 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
}
|
||||
});
|
||||
|
||||
return certificateTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||
body: z.object({
|
||||
caId: z.string().describe(CERTIFICATE_TEMPLATES.CREATE.caId),
|
||||
pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.CREATE.pkiCollectionId),
|
||||
name: slugSchema().describe(CERTIFICATE_TEMPLATES.CREATE.name),
|
||||
commonName: validateTemplateRegexField.describe(CERTIFICATE_TEMPLATES.CREATE.commonName),
|
||||
subjectAlternativeName: validateTemplateRegexField.describe(
|
||||
CERTIFICATE_TEMPLATES.CREATE.subjectAlternativeName
|
||||
),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl),
|
||||
keyUsages: z
|
||||
.nativeEnum(CertKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT])
|
||||
.describe(CERTIFICATE_TEMPLATES.CREATE.keyUsages),
|
||||
extendedKeyUsages: z
|
||||
.nativeEnum(CertExtendedKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.default([])
|
||||
.describe(CERTIFICATE_TEMPLATES.CREATE.extendedKeyUsages)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedCertificateTemplate
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificateTemplate = await server.services.certificateTemplate.createCertTemplate({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateTemplate.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
caId: certificateTemplate.caId,
|
||||
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
|
||||
name: certificateTemplate.name,
|
||||
commonName: certificateTemplate.commonName,
|
||||
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
|
||||
ttl: certificateTemplate.ttl,
|
||||
projectId: certificateTemplate.projectId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateTemplate;
|
||||
return { certificateTemplate };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:certificateTemplateId",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||
body: z.object({
|
||||
caId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.caId),
|
||||
pkiCollectionId: z.string().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.pkiCollectionId),
|
||||
name: slugSchema().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.name),
|
||||
commonName: validateTemplateRegexField.optional().describe(CERTIFICATE_TEMPLATES.UPDATE.commonName),
|
||||
subjectAlternativeName: validateTemplateRegexField
|
||||
.optional()
|
||||
.describe(CERTIFICATE_TEMPLATES.UPDATE.subjectAlternativeName),
|
||||
ttl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.optional()
|
||||
.describe(CERTIFICATE_TEMPLATES.UPDATE.ttl),
|
||||
keyUsages: z.nativeEnum(CertKeyUsage).array().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.keyUsages),
|
||||
extendedKeyUsages: z
|
||||
.nativeEnum(CertExtendedKeyUsage)
|
||||
.array()
|
||||
.optional()
|
||||
.describe(CERTIFICATE_TEMPLATES.UPDATE.extendedKeyUsages)
|
||||
}),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
body: updateCertificateTemplateV2Schema,
|
||||
response: {
|
||||
200: sanitizedCertificateTemplate
|
||||
200: z.object({
|
||||
certificateTemplate: certificateTemplateV2ResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificateTemplate = await server.services.certificateTemplate.updateCertTemplate({
|
||||
...req.body,
|
||||
id: req.params.certificateTemplateId,
|
||||
const certificateTemplate = await server.services.certificateTemplateV2.updateTemplateV2({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
actorAuthMethod: req.permission.authMethod!,
|
||||
actorOrgId: req.permission.orgId,
|
||||
templateId: req.params.id,
|
||||
data: req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@@ -186,23 +312,18 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
name: certificateTemplate.name,
|
||||
caId: certificateTemplate.caId,
|
||||
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
|
||||
commonName: certificateTemplate.commonName,
|
||||
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
|
||||
ttl: certificateTemplate.ttl
|
||||
name: certificateTemplate.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificateTemplate;
|
||||
return { certificateTemplate };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:certificateTemplateId",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
@@ -210,20 +331,22 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.DELETE.certificateTemplateId)
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedCertificateTemplate
|
||||
200: z.object({
|
||||
certificateTemplate: certificateTemplateV2ResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificateTemplate = await server.services.certificateTemplate.deleteCertTemplate({
|
||||
id: req.params.certificateTemplateId,
|
||||
const certificateTemplate = await server.services.certificateTemplateV2.deleteTemplateV2({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
actorAuthMethod: req.permission.authMethod!,
|
||||
actorOrgId: req.permission.orgId,
|
||||
templateId: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@@ -238,158 +361,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
}
|
||||
});
|
||||
|
||||
return certificateTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||
description: "Create Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
caChain: z.string().trim().optional(),
|
||||
passphrase: z.string().min(1),
|
||||
isEnabled: z.boolean().default(true),
|
||||
disableBootstrapCertValidation: z.boolean().default(false)
|
||||
})
|
||||
.refine(
|
||||
({ caChain, disableBootstrapCertValidation }) =>
|
||||
disableBootstrapCertValidation || (!disableBootstrapCertValidation && caChain),
|
||||
"CA chain is required"
|
||||
),
|
||||
response: {
|
||||
200: sanitizedEstConfig
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.createEstConfiguration({
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId,
|
||||
isEnabled: estConfig.isEnabled as boolean
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||
description: "Update Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().optional(),
|
||||
passphrase: z.string().min(1).optional(),
|
||||
disableBootstrapCertValidation: z.boolean().optional(),
|
||||
isEnabled: z.boolean().optional()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.updateEstConfiguration({
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId,
|
||||
isEnabled: estConfig.isEnabled as boolean
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||
description: "Get Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig.extend({
|
||||
caChain: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
|
||||
isInternal: false,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
return { certificateTemplate };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
680
backend/src/server/routes/v1/deprecated-certificate-router.ts
Normal file
680
backend/src/server/routes/v1/deprecated-certificate-router.ts
Normal file
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
205
backend/src/server/routes/v1/deprecated-pki-alert-router.ts
Normal file
205
backend/src/server/routes/v1/deprecated-pki-alert-router.ts
Normal file
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -11,10 +11,15 @@ import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
|
||||
import { registerGeneralCertificateAuthorityRouter } from "./certificate-authority-routers/general-certificate-authority-router";
|
||||
import { registerCertificateProfilesRouter } from "./certificate-profiles-router";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateRouter } from "./certificate-router";
|
||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||
import { DEPRECATED_CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./deprecated-certificate-authority-routers";
|
||||
import { registerDeprecatedCertRouter } from "./deprecated-certificate-router";
|
||||
import { registerDeprecatedCertificateTemplateRouter } from "./deprecated-certificate-template-router";
|
||||
import { registerDeprecatedIdentityProjectMembershipRouter } from "./deprecated-identity-project-membership-router";
|
||||
import { registerDeprecatedPkiAlertRouter } from "./deprecated-pki-alert-router";
|
||||
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
|
||||
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
|
||||
import { registerDeprecatedProjectRouter } from "./deprecated-project-router";
|
||||
@@ -150,21 +155,54 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.register(
|
||||
async (pkiRouter) => {
|
||||
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
||||
await pkiRouter.register(
|
||||
async (caRouter) => {
|
||||
for await (const [caType, router] of Object.entries(CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP)) {
|
||||
await caRouter.register(router, { prefix: `/${caType}` });
|
||||
}
|
||||
|
||||
await caRouter.register(registerGeneralCertificateAuthorityRouter);
|
||||
},
|
||||
{
|
||||
prefix: "/ca"
|
||||
}
|
||||
);
|
||||
await pkiRouter.register(registerCertificateRouter, { prefix: "/certificates" });
|
||||
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await pkiRouter.register(registerCertificateProfilesRouter, { prefix: "/certificate-profiles" });
|
||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||
await pkiRouter.register(
|
||||
async (pkiSyncRouter) => {
|
||||
await pkiSyncRouter.register(registerPkiSyncRouter);
|
||||
for await (const [destination, router] of Object.entries(PKI_SYNC_REGISTER_ROUTER_MAP)) {
|
||||
await pkiSyncRouter.register(router, { prefix: `/${destination}` });
|
||||
}
|
||||
},
|
||||
{ prefix: "/syncs" }
|
||||
);
|
||||
},
|
||||
{ prefix: "/cert-manager" }
|
||||
);
|
||||
|
||||
// NOTE: THESE /pki/* ENDPOINTS ARE TO BE DEPRECATED IN FAVOR OF /cert-manager/*
|
||||
// DO NOT EXTEND THEM ANYMORE!!!
|
||||
await server.register(
|
||||
async (pkiRouter) => {
|
||||
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
||||
await pkiRouter.register(
|
||||
async (caRouter) => {
|
||||
for await (const [caType, router] of Object.entries(DEPRECATED_CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP)) {
|
||||
await caRouter.register(router, { prefix: `/${caType}` });
|
||||
}
|
||||
},
|
||||
{
|
||||
prefix: "/ca"
|
||||
}
|
||||
);
|
||||
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
||||
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await pkiRouter.register(registerDeprecatedCertRouter, { prefix: "/certificates" });
|
||||
await pkiRouter.register(registerDeprecatedCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await pkiRouter.register(registerCertificateProfilesRouter, { prefix: "/certificate-profiles" });
|
||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||
await pkiRouter.register(registerDeprecatedPkiAlertRouter, { prefix: "/alerts" });
|
||||
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
||||
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
|
||||
await pkiRouter.register(
|
||||
|
||||
@@ -10,7 +10,12 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
|
||||
import { Integrations } from "@app/services/integration-auth/integration-list";
|
||||
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
|
||||
import {
|
||||
PostHogEventTypes,
|
||||
TIntegrationCreatedEvent,
|
||||
TIntegrationDeletedEvent,
|
||||
TIntegrationSyncedEvent
|
||||
} from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
import {} from "../sanitizedSchemas";
|
||||
|
||||
@@ -288,31 +293,47 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
});
|
||||
|
||||
const deleteIntegrationEventProperty = shake({
|
||||
integrationId: integration.id,
|
||||
integration: integration.integration,
|
||||
environment: integration.environment.slug,
|
||||
secretPath: integration.secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
}) as TIntegrationDeletedEvent["properties"];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: integration.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_INTEGRATION,
|
||||
// eslint-disable-next-line
|
||||
metadata: shake({
|
||||
integrationId: integration.id,
|
||||
integration: integration.integration,
|
||||
environment: integration.environment.slug,
|
||||
secretPath: integration.secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region,
|
||||
metadata: {
|
||||
...deleteIntegrationEventProperty,
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
// eslint-disable-next-line
|
||||
}) as any
|
||||
} as any
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IntegrationDeleted,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
...deleteIntegrationEventProperty,
|
||||
projectId: integration.projectId,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return { integration };
|
||||
}
|
||||
});
|
||||
@@ -351,28 +372,41 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
id: req.params.integrationId
|
||||
});
|
||||
|
||||
const syncIntegrationEventProperty = shake({
|
||||
integrationId: integration.id,
|
||||
integration: integration.integration,
|
||||
environment: integration.environment.slug,
|
||||
secretPath: integration.secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
}) as TIntegrationSyncedEvent["properties"];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: integration.projectId,
|
||||
event: {
|
||||
type: EventType.MANUAL_SYNC_INTEGRATION,
|
||||
// eslint-disable-next-line
|
||||
metadata: shake({
|
||||
integrationId: integration.id,
|
||||
integration: integration.integration,
|
||||
environment: integration.environment.slug,
|
||||
secretPath: integration.secretPath,
|
||||
url: integration.url,
|
||||
app: integration.app,
|
||||
appId: integration.appId,
|
||||
targetEnvironment: integration.targetEnvironment,
|
||||
targetEnvironmentId: integration.targetEnvironmentId,
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
// eslint-disable-next-line
|
||||
}) as any
|
||||
metadata: syncIntegrationEventProperty as any
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IntegrationSynced,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
...syncIntegrationEventProperty,
|
||||
projectId: integration.projectId,
|
||||
isManualSync: true,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiAlertsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ALERTS, ApiDocsTags } from "@app/lib/api-docs";
|
||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { PkiAlertEventType } from "@app/services/pki-alert-v2/pki-alert-v2-types";
|
||||
import {
|
||||
CreatePkiAlertV2Schema,
|
||||
createSecureAlertBeforeValidator,
|
||||
PkiAlertChannelType,
|
||||
PkiAlertEventType,
|
||||
PkiFilterRuleSchema,
|
||||
UpdatePkiAlertV2Schema
|
||||
} from "@app/services/pki-alert-v2/pki-alert-v2-types";
|
||||
|
||||
export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -17,25 +23,41 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Create a new PKI alert",
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
description: "Create PKI alert",
|
||||
body: z.object({
|
||||
projectId: z.string().trim().describe(ALERTS.CREATE.projectId),
|
||||
pkiCollectionId: z.string().trim().describe(ALERTS.CREATE.pkiCollectionId),
|
||||
name: z.string().trim().describe(ALERTS.CREATE.name),
|
||||
alertBeforeDays: z.number().describe(ALERTS.CREATE.alertBeforeDays),
|
||||
emails: z
|
||||
.array(z.string().trim().email({ message: "Invalid email address" }))
|
||||
.min(1, { message: "You must specify at least 1 email" })
|
||||
.max(5, { message: "You can specify a maximum of 5 emails" })
|
||||
.describe(ALERTS.CREATE.emails)
|
||||
body: CreatePkiAlertV2Schema.extend({
|
||||
projectId: z.string().uuid().describe("Project ID")
|
||||
}),
|
||||
response: {
|
||||
200: PkiAlertsSchema
|
||||
200: z.object({
|
||||
alert: z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
enabled: z.boolean(),
|
||||
projectId: z.string().uuid(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.nativeEnum(PkiAlertChannelType),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const alert = await server.services.pkiAlert.createPkiAlert({
|
||||
const alert = await server.services.pkiAlertV2.createAlert({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -45,21 +67,80 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: alert.projectId,
|
||||
projectId: req.body.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PKI_ALERT,
|
||||
metadata: {
|
||||
pkiAlertId: alert.id,
|
||||
pkiCollectionId: alert.pkiCollectionId,
|
||||
name: alert.name,
|
||||
alertBefore: alert.alertBeforeDays.toString(),
|
||||
eventType: PkiAlertEventType.EXPIRATION,
|
||||
recipientEmails: alert.recipientEmails
|
||||
eventType: alert.eventType,
|
||||
alertBefore: alert.alertBefore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return alert;
|
||||
return { alert };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "List PKI alerts for a project",
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
querystring: z.object({
|
||||
projectId: z.string().uuid(),
|
||||
search: z.string().optional(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType).optional(),
|
||||
enabled: z.coerce.boolean().optional(),
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
offset: z.coerce.number().min(0).default(0)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
alerts: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
enabled: z.boolean(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.nativeEnum(PkiAlertChannelType),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
),
|
||||
total: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const alerts = await server.services.pkiAlertV2.listAlerts({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
return alerts;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,17 +152,41 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Get a PKI alert by ID",
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
description: "Get PKI alert",
|
||||
params: z.object({
|
||||
alertId: z.string().trim().describe(ALERTS.GET.alertId)
|
||||
alertId: z.string().uuid().describe("Alert ID")
|
||||
}),
|
||||
response: {
|
||||
200: PkiAlertsSchema
|
||||
200: z.object({
|
||||
alert: z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
enabled: z.boolean(),
|
||||
projectId: z.string().uuid(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.nativeEnum(PkiAlertChannelType),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const alert = await server.services.pkiAlert.getPkiAlertById({
|
||||
const alert = await server.services.pkiAlertV2.getAlertById({
|
||||
alertId: req.params.alertId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -100,7 +205,7 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
return alert;
|
||||
return { alert };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,32 +213,46 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
method: "PATCH",
|
||||
url: "/:alertId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Update a PKI alert",
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
description: "Update PKI alert",
|
||||
params: z.object({
|
||||
alertId: z.string().trim().describe(ALERTS.UPDATE.alertId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().optional().describe(ALERTS.UPDATE.name),
|
||||
alertBeforeDays: z.number().optional().describe(ALERTS.UPDATE.alertBeforeDays),
|
||||
pkiCollectionId: z.string().trim().optional().describe(ALERTS.UPDATE.pkiCollectionId),
|
||||
emails: z
|
||||
.array(z.string().trim().email({ message: "Invalid email address" }))
|
||||
.min(1, { message: "You must specify at least 1 email" })
|
||||
.max(5, { message: "You can specify a maximum of 5 emails" })
|
||||
.optional()
|
||||
.describe(ALERTS.UPDATE.emails)
|
||||
alertId: z.string().uuid().describe("Alert ID")
|
||||
}),
|
||||
body: UpdatePkiAlertV2Schema,
|
||||
response: {
|
||||
200: PkiAlertsSchema
|
||||
200: z.object({
|
||||
alert: z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
enabled: z.boolean(),
|
||||
projectId: z.string().uuid(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.nativeEnum(PkiAlertChannelType),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const alert = await server.services.pkiAlert.updatePkiAlert({
|
||||
const alert = await server.services.pkiAlertV2.updateAlert({
|
||||
alertId: req.params.alertId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -149,16 +268,14 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
type: EventType.UPDATE_PKI_ALERT,
|
||||
metadata: {
|
||||
pkiAlertId: alert.id,
|
||||
pkiCollectionId: alert.pkiCollectionId,
|
||||
name: alert.name,
|
||||
alertBefore: alert.alertBeforeDays.toString(),
|
||||
eventType: PkiAlertEventType.EXPIRATION,
|
||||
recipientEmails: alert.recipientEmails
|
||||
eventType: alert.eventType,
|
||||
alertBefore: alert.alertBefore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return alert;
|
||||
return { alert };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -170,17 +287,41 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Delete a PKI alert",
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
description: "Delete PKI alert",
|
||||
params: z.object({
|
||||
alertId: z.string().trim().describe(ALERTS.DELETE.alertId)
|
||||
alertId: z.string().uuid().describe("Alert ID")
|
||||
}),
|
||||
response: {
|
||||
200: PkiAlertsSchema
|
||||
200: z.object({
|
||||
alert: z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
eventType: z.nativeEnum(PkiAlertEventType),
|
||||
alertBefore: z.string(),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
enabled: z.boolean(),
|
||||
projectId: z.string().uuid(),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
channelType: z.nativeEnum(PkiAlertChannelType),
|
||||
config: z.record(z.any()),
|
||||
enabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const alert = await server.services.pkiAlert.deletePkiAlert({
|
||||
const alert = await server.services.pkiAlertV2.deleteAlert({
|
||||
alertId: req.params.alertId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -199,7 +340,111 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
return alert;
|
||||
return { alert };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:alertId/certificates",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "List certificates that match an alert's filter rules",
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
params: z.object({
|
||||
alertId: z.string().uuid().describe("Alert ID")
|
||||
}),
|
||||
querystring: z.object({
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
offset: z.coerce.number().min(0).default(0)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificates: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
serialNumber: z.string(),
|
||||
commonName: z.string(),
|
||||
san: z.array(z.string()),
|
||||
profileName: z.string().nullable(),
|
||||
enrollmentType: z.string().nullable(),
|
||||
notBefore: z.date(),
|
||||
notAfter: z.date(),
|
||||
status: z.string()
|
||||
})
|
||||
),
|
||||
total: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const result = await server.services.pkiAlertV2.listMatchingCertificates({
|
||||
alertId: req.params.alertId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/preview/certificates",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Preview certificates that would match the given filter rules",
|
||||
tags: [ApiDocsTags.PkiAlerting],
|
||||
body: z.object({
|
||||
projectId: z.string().uuid().describe("Project ID"),
|
||||
filters: z.array(PkiFilterRuleSchema),
|
||||
alertBefore: z
|
||||
.string()
|
||||
.refine(createSecureAlertBeforeValidator(), "Must be in format like '30d', '1w', '3m', '1y'")
|
||||
.describe("Alert timing (e.g., '30d', '1w')"),
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
offset: z.coerce.number().min(0).default(0)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificates: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
serialNumber: z.string(),
|
||||
commonName: z.string(),
|
||||
san: z.array(z.string()),
|
||||
profileName: z.string().nullable(),
|
||||
enrollmentType: z.string().nullable(),
|
||||
notBefore: z.date(),
|
||||
notAfter: z.date(),
|
||||
status: z.string()
|
||||
})
|
||||
),
|
||||
total: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const result = await server.services.pkiAlertV2.listCurrentMatchingCertificates({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { registerCertificateTemplatesV2Router } from "./certificate-templates-v2-router";
|
||||
import { registerCertificateTemplatesV2Router } from "./deprecated-certificate-templates-v2-router";
|
||||
import { registerDeprecatedGroupProjectRouter } from "./deprecated-group-project-router";
|
||||
import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-router";
|
||||
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { registerCertificatesRouter } from "./certificates-router";
|
||||
import { registerCertificatesRouter } from "./deprecated-certificates-router";
|
||||
import { registerDeprecatedSecretRouter } from "./deprecated-secret-router";
|
||||
import { registerExternalMigrationRouter } from "./external-migration-router";
|
||||
import { registerLoginRouter } from "./login-router";
|
||||
|
||||
@@ -29,6 +29,7 @@ export enum AppConnection {
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway",
|
||||
Bitbucket = "bitbucket",
|
||||
|
||||
@@ -88,6 +88,11 @@ import {
|
||||
getDigitalOceanConnectionListItem,
|
||||
validateDigitalOceanConnectionCredentials
|
||||
} from "./digital-ocean";
|
||||
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy/dns-made-easy-connection-enum";
|
||||
import {
|
||||
getDNSMadeEasyConnectionListItem,
|
||||
validateDNSMadeEasyConnectionCredentials
|
||||
} from "./dns-made-easy/dns-made-easy-connection-fns";
|
||||
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
|
||||
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
||||
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
||||
@@ -171,7 +176,8 @@ const PKI_APP_CONNECTIONS = [
|
||||
AppConnection.Cloudflare,
|
||||
AppConnection.AzureADCS,
|
||||
AppConnection.AzureKeyVault,
|
||||
AppConnection.Chef
|
||||
AppConnection.Chef,
|
||||
AppConnection.DNSMadeEasy
|
||||
];
|
||||
|
||||
export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
@@ -207,6 +213,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
getFlyioConnectionListItem(),
|
||||
getGitLabConnectionListItem(),
|
||||
getCloudflareConnectionListItem(),
|
||||
getDNSMadeEasyConnectionListItem(),
|
||||
getZabbixConnectionListItem(),
|
||||
getRailwayConnectionListItem(),
|
||||
getBitbucketConnectionListItem(),
|
||||
@@ -339,6 +346,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.DNSMadeEasy]: validateDNSMadeEasyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
@@ -395,6 +403,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case OktaConnectionMethod.ApiToken:
|
||||
case LaravelForgeConnectionMethod.ApiToken:
|
||||
return "API Token";
|
||||
case DNSMadeEasyConnectionMethod.APIKeySecret:
|
||||
return "API Key & Secret";
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
case MySqlConnectionMethod.UsernameAndPassword:
|
||||
@@ -483,6 +493,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.DNSMadeEasy]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Flyio]: "Fly.io",
|
||||
[AppConnection.GitLab]: "GitLab",
|
||||
[AppConnection.Cloudflare]: "Cloudflare",
|
||||
[AppConnection.DNSMadeEasy]: "DNS Made Easy",
|
||||
[AppConnection.Zabbix]: "Zabbix",
|
||||
[AppConnection.Railway]: "Railway",
|
||||
[AppConnection.Bitbucket]: "Bitbucket",
|
||||
@@ -77,6 +78,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.DNSMadeEasy]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Railway]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
|
||||
|
||||
@@ -72,6 +72,8 @@ import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
import { ValidateDNSMadeEasyConnectionCredentialsSchema } from "./dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { dnsMadeEasyConnectionService } from "./dns-made-easy/dns-made-easy-connection-service";
|
||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
||||
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
|
||||
@@ -167,6 +169,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
||||
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
|
||||
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
|
||||
[AppConnection.DNSMadeEasy]: ValidateDNSMadeEasyConnectionCredentialsSchema,
|
||||
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
|
||||
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
|
||||
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
|
||||
@@ -875,6 +878,7 @@ export const appConnectionServiceFactory = ({
|
||||
flyio: flyioConnectionService(connectAppConnectionById),
|
||||
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
cloudflare: cloudflareConnectionService(connectAppConnectionById),
|
||||
dnsMadeEasy: dnsMadeEasyConnectionService(connectAppConnectionById),
|
||||
zabbix: zabbixConnectionService(connectAppConnectionById),
|
||||
railway: railwayConnectionService(connectAppConnectionById),
|
||||
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
TOracleDBConnectionInput,
|
||||
TValidateOracleDBConnectionCredentialsSchema
|
||||
} from "@app/ee/services/app-connections/oracledb";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -106,6 +106,12 @@ import {
|
||||
TDigitalOceanConnectionInput,
|
||||
TValidateDigitalOceanCredentialsSchema
|
||||
} from "./digital-ocean";
|
||||
import {
|
||||
TDNSMadeEasyConnection,
|
||||
TDNSMadeEasyConnectionConfig,
|
||||
TDNSMadeEasyConnectionInput,
|
||||
TValidateDNSMadeEasyConnectionCredentialsSchema
|
||||
} from "./dns-made-easy/dns-made-easy-connection-types";
|
||||
import {
|
||||
TFlyioConnection,
|
||||
TFlyioConnectionConfig,
|
||||
@@ -279,6 +285,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TGitLabConnection
|
||||
| TCloudflareConnection
|
||||
| TBitbucketConnection
|
||||
| TDNSMadeEasyConnection
|
||||
| TZabbixConnection
|
||||
| TRailwayConnection
|
||||
| TChecklyConnection
|
||||
@@ -328,6 +335,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TGitLabConnectionInput
|
||||
| TCloudflareConnectionInput
|
||||
| TBitbucketConnectionInput
|
||||
| TDNSMadeEasyConnectionInput
|
||||
| TZabbixConnectionInput
|
||||
| TRailwayConnectionInput
|
||||
| TChecklyConnectionInput
|
||||
@@ -395,6 +403,7 @@ export type TAppConnectionConfig =
|
||||
| TGitLabConnectionConfig
|
||||
| TCloudflareConnectionConfig
|
||||
| TBitbucketConnectionConfig
|
||||
| TDNSMadeEasyConnectionConfig
|
||||
| TZabbixConnectionConfig
|
||||
| TRailwayConnectionConfig
|
||||
| TChecklyConnectionConfig
|
||||
@@ -439,6 +448,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateGitLabConnectionCredentialsSchema
|
||||
| TValidateCloudflareConnectionCredentialsSchema
|
||||
| TValidateBitbucketConnectionCredentialsSchema
|
||||
| TValidateDNSMadeEasyConnectionCredentialsSchema
|
||||
| TValidateZabbixConnectionCredentialsSchema
|
||||
| TValidateRailwayConnectionCredentialsSchema
|
||||
| TValidateChecklyConnectionCredentialsSchema
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum DNSMadeEasyConnectionMethod {
|
||||
APIKeySecret = "api-key-secret"
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
|
||||
import {
|
||||
TDNSMadeEasyConnection,
|
||||
TDNSMadeEasyConnectionConfig,
|
||||
TDNSMadeEasyZone
|
||||
} from "./dns-made-easy-connection-types";
|
||||
|
||||
interface DNSMadeEasyApiResponse {
|
||||
totalRecords: number;
|
||||
totalPages: number;
|
||||
data: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}>;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export const getDNSMadeEasyUrl = (path: string) => {
|
||||
const appCfg = getConfig();
|
||||
return `${appCfg.DNS_MADE_EASY_SANDBOX_ENABLED ? IntegrationUrls.DNS_MADE_EASY_SANDBOX_API_URL : IntegrationUrls.DNS_MADE_EASY_API_URL}${path}`;
|
||||
};
|
||||
|
||||
export const makeDNSMadeEasyAuthHeaders = (
|
||||
apiKey: string,
|
||||
secretKey: string,
|
||||
currentDate?: Date
|
||||
): Record<string, string> => {
|
||||
// Format date as "Day, DD Mon YYYY HH:MM:SS GMT" (e.g., "Mon, 01 Jan 2024 12:00:00 GMT")
|
||||
const requestDate = (currentDate ?? new Date()).toUTCString();
|
||||
|
||||
// Generate HMAC-SHA1 signature
|
||||
const hmac = crypto.nativeCrypto.createHmac("sha1", secretKey);
|
||||
hmac.update(requestDate);
|
||||
const hmacSignature = hmac.digest("hex");
|
||||
|
||||
return {
|
||||
"x-dnsme-apiKey": apiKey,
|
||||
"x-dnsme-hmac": hmacSignature,
|
||||
"x-dnsme-requestDate": requestDate
|
||||
};
|
||||
};
|
||||
|
||||
export const getDNSMadeEasyConnectionListItem = () => {
|
||||
return {
|
||||
name: "DNS Made Easy" as const,
|
||||
app: AppConnection.DNSMadeEasy as const,
|
||||
methods: Object.values(DNSMadeEasyConnectionMethod) as [DNSMadeEasyConnectionMethod.APIKeySecret]
|
||||
};
|
||||
};
|
||||
|
||||
export const listDNSMadeEasyZones = async (appConnection: TDNSMadeEasyConnection): Promise<TDNSMadeEasyZone[]> => {
|
||||
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = appConnection;
|
||||
|
||||
try {
|
||||
const allZones: TDNSMadeEasyZone[] = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 1;
|
||||
|
||||
// Fetch all pages of zones
|
||||
while (currentPage < totalPages) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resp = await request.get<DNSMadeEasyApiResponse>(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: {
|
||||
page: currentPage
|
||||
}
|
||||
});
|
||||
|
||||
if (resp.data?.data) {
|
||||
// Map the API response to TDNSMadeEasyZone format
|
||||
const zones = resp.data.data.map((zone) => ({
|
||||
id: String(zone.id),
|
||||
name: zone.name
|
||||
}));
|
||||
allZones.push(...zones);
|
||||
|
||||
// Update pagination info
|
||||
totalPages = resp.data.totalPages || 1;
|
||||
currentPage += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allZones;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error listing DNS Made Easy zones");
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to list DNS Made Easy zones: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list DNS Made Easy zones"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const listDNSMadeEasyRecords = async (
|
||||
appConnection: TDNSMadeEasyConnection,
|
||||
options: { zoneId: string; type?: string; name?: string }
|
||||
): Promise<DNSMadeEasyApiResponse["data"]> => {
|
||||
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = appConnection;
|
||||
const { zoneId, type, name } = options;
|
||||
|
||||
try {
|
||||
const allRecords: DNSMadeEasyApiResponse["data"] = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 1;
|
||||
|
||||
// Fetch all pages of records
|
||||
while (currentPage < totalPages) {
|
||||
// Build query parameters
|
||||
const queryParams: Record<string, string | number> = {};
|
||||
if (type) {
|
||||
queryParams.type = type;
|
||||
}
|
||||
if (name) {
|
||||
queryParams.recordName = name;
|
||||
}
|
||||
queryParams.page = currentPage;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resp = await request.get<DNSMadeEasyApiResponse>(
|
||||
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(zoneId)}/records`),
|
||||
{
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: queryParams
|
||||
}
|
||||
);
|
||||
|
||||
if (resp.data?.data) {
|
||||
allRecords.push(...resp.data.data);
|
||||
|
||||
// Update pagination info
|
||||
totalPages = resp.data.totalPages || 1;
|
||||
currentPage += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allRecords;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error listing DNS Made Easy records");
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to list DNS Made Easy records: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list DNS Made Easy records"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateDNSMadeEasyConnectionCredentials = async (config: TDNSMadeEasyConnectionConfig) => {
|
||||
if (config.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
|
||||
const { apiKey, secretKey } = config.credentials;
|
||||
|
||||
try {
|
||||
const resp = await request.get(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
if (resp.status !== 200) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: Invalid API credentials provided."
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to validate credentials: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
logger.error(error, "Error validating DNS Made Easy connection credentials");
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
|
||||
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
|
||||
|
||||
export const DNSMadeEasyConnectionApiKeyCredentialsSchema = z.object({
|
||||
apiKey: z.string().trim().min(1, "API key required").max(256, "API key cannot exceed 256 characters"),
|
||||
secretKey: z.string().trim().min(1, "Secret key required").max(256, "Secret key cannot exceed 256 characters")
|
||||
});
|
||||
|
||||
const BaseDNSMadeEasyConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.DNSMadeEasy)
|
||||
});
|
||||
|
||||
export const DNSMadeEasyConnectionSchema = BaseDNSMadeEasyConnectionSchema.extend({
|
||||
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedDNSMadeEasyConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseDNSMadeEasyConnectionSchema.extend({
|
||||
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.pick({ apiKey: true })
|
||||
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy]} (API Key)` }))
|
||||
]);
|
||||
|
||||
export const ValidateDNSMadeEasyConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(DNSMadeEasyConnectionMethod.APIKeySecret)
|
||||
.describe(AppConnections.CREATE(AppConnection.DNSMadeEasy).method),
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.DNSMadeEasy).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateDNSMadeEasyConnectionSchema = ValidateDNSMadeEasyConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy)
|
||||
);
|
||||
|
||||
export const UpdateDNSMadeEasyConnectionSchema = z
|
||||
.object({
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.DNSMadeEasy).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy));
|
||||
|
||||
export const DNSMadeEasyConnectionListItemSchema = z
|
||||
.object({
|
||||
name: z.literal("DNS Made Easy"),
|
||||
app: z.literal(AppConnection.DNSMadeEasy),
|
||||
methods: z.nativeEnum(DNSMadeEasyConnectionMethod).array()
|
||||
})
|
||||
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy] }));
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listDNSMadeEasyZones } from "./dns-made-easy-connection-fns";
|
||||
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TDNSMadeEasyConnection>;
|
||||
|
||||
export const dnsMadeEasyConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listZones = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.DNSMadeEasy, connectionId, actor);
|
||||
try {
|
||||
const zones = await listDNSMadeEasyZones(appConnection);
|
||||
return zones;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
`Failed to list DNS Made Easy zones for DNS Made Easy connection [connectionId=${connectionId}]`
|
||||
);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list DNS Made Easy zones: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listZones
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateDNSMadeEasyConnectionSchema,
|
||||
DNSMadeEasyConnectionSchema,
|
||||
ValidateDNSMadeEasyConnectionCredentialsSchema
|
||||
} from "./dns-made-easy-connection-schema";
|
||||
|
||||
export type TDNSMadeEasyConnection = z.infer<typeof DNSMadeEasyConnectionSchema>;
|
||||
|
||||
export type TDNSMadeEasyConnectionInput = z.infer<typeof CreateDNSMadeEasyConnectionSchema> & {
|
||||
app: AppConnection.DNSMadeEasy;
|
||||
};
|
||||
|
||||
export type TValidateDNSMadeEasyConnectionCredentialsSchema = typeof ValidateDNSMadeEasyConnectionCredentialsSchema;
|
||||
|
||||
export type TDNSMadeEasyConnectionConfig = DiscriminativePick<
|
||||
TDNSMadeEasyConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TDNSMadeEasyZone = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum AcmeDnsProvider {
|
||||
Route53 = "route53",
|
||||
Cloudflare = "cloudflare"
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
|
||||
import { TDNSMadeEasyConnection } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-types";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
TUpdateAcmeCertificateAuthorityDTO
|
||||
} from "./acme-certificate-authority-types";
|
||||
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
|
||||
import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy";
|
||||
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
||||
|
||||
const parseTtlToDays = (ttl: string): number => {
|
||||
@@ -178,6 +180,22 @@ export const castDbEntryToAcmeCertificateAuthority = (
|
||||
};
|
||||
};
|
||||
|
||||
const getAcmeChallengeRecord = (
|
||||
provider: AcmeDnsProvider,
|
||||
identifierValue: string,
|
||||
keyAuthorization: string
|
||||
): { recordName: string; recordValue: string } => {
|
||||
let recordName: string;
|
||||
if (provider === AcmeDnsProvider.DNSMadeEasy) {
|
||||
// For DNS Made Easy, we don't need to provide the domain name in the record name.
|
||||
recordName = "_acme-challenge";
|
||||
} else {
|
||||
recordName = `_acme-challenge.${identifierValue}`; // e.g., "_acme-challenge.example.com"
|
||||
}
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
return { recordName, recordValue };
|
||||
};
|
||||
|
||||
export const orderCertificate = async (
|
||||
{
|
||||
caId,
|
||||
@@ -312,8 +330,11 @@ export const orderCertificate = async (
|
||||
throw new Error("Unsupported challenge type");
|
||||
}
|
||||
|
||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
const { recordName, recordValue } = getAcmeChallengeRecord(
|
||||
acmeCa.configuration.dnsProviderConfig.provider,
|
||||
authz.identifier.value,
|
||||
keyAuthorization
|
||||
);
|
||||
|
||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||
case AcmeDnsProvider.Route53: {
|
||||
@@ -334,14 +355,26 @@ export const orderCertificate = async (
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AcmeDnsProvider.DNSMadeEasy: {
|
||||
await dnsMadeEasyInsertTxtRecord(
|
||||
connection as TDNSMadeEasyConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
const { recordName, recordValue } = getAcmeChallengeRecord(
|
||||
acmeCa.configuration.dnsProviderConfig.provider,
|
||||
authz.identifier.value,
|
||||
keyAuthorization
|
||||
);
|
||||
|
||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||
case AcmeDnsProvider.Route53: {
|
||||
@@ -362,6 +395,15 @@ export const orderCertificate = async (
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AcmeDnsProvider.DNSMadeEasy: {
|
||||
await dnsMadeEasyDeleteTxtRecord(
|
||||
connection as TDNSMadeEasyConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||
}
|
||||
@@ -477,7 +519,6 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
name,
|
||||
projectId,
|
||||
configuration,
|
||||
enableDirectIssuance,
|
||||
actor,
|
||||
status
|
||||
}: {
|
||||
@@ -485,7 +526,6 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
name: string;
|
||||
projectId: string;
|
||||
configuration: TCreateAcmeCertificateAuthorityDTO["configuration"];
|
||||
enableDirectIssuance: boolean;
|
||||
actor: OrgServiceActor;
|
||||
}) => {
|
||||
if (crypto.isFipsModeEnabled()) {
|
||||
@@ -513,6 +553,12 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (dnsProviderConfig.provider === AcmeDnsProvider.DNSMadeEasy && appConnection.app !== AppConnection.DNSMadeEasy) {
|
||||
throw new BadRequestError({
|
||||
message: `App connection with ID '${dnsAppConnectionId}' is not a DNS Made Easy connection`
|
||||
});
|
||||
}
|
||||
|
||||
// validates permission to connect
|
||||
await appConnectionService.validateAppConnectionUsageById(
|
||||
appConnection.app as AppConnection,
|
||||
@@ -525,7 +571,7 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
const ca = await certificateAuthorityDAL.create(
|
||||
{
|
||||
projectId,
|
||||
enableDirectIssuance,
|
||||
enableDirectIssuance: false,
|
||||
name,
|
||||
status
|
||||
},
|
||||
@@ -573,14 +619,12 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
id,
|
||||
status,
|
||||
configuration,
|
||||
enableDirectIssuance,
|
||||
actor,
|
||||
name
|
||||
}: {
|
||||
id: string;
|
||||
status?: CaStatus;
|
||||
configuration: TUpdateAcmeCertificateAuthorityDTO["configuration"];
|
||||
enableDirectIssuance?: boolean;
|
||||
actor: OrgServiceActor;
|
||||
name?: string;
|
||||
}) => {
|
||||
@@ -608,6 +652,15 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
dnsProviderConfig.provider === AcmeDnsProvider.DNSMadeEasy &&
|
||||
appConnection.app !== AppConnection.DNSMadeEasy
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `App connection with ID '${dnsAppConnectionId}' is not a DNS Made Easy connection`
|
||||
});
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(id);
|
||||
|
||||
if (!ca) {
|
||||
@@ -641,13 +694,12 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (name || status || enableDirectIssuance) {
|
||||
if (name || status) {
|
||||
await certificateAuthorityDAL.updateById(
|
||||
id,
|
||||
{
|
||||
name,
|
||||
status,
|
||||
enableDirectIssuance
|
||||
status
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import {
|
||||
getDNSMadeEasyUrl,
|
||||
listDNSMadeEasyRecords,
|
||||
makeDNSMadeEasyAuthHeaders
|
||||
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-fns";
|
||||
import { TDNSMadeEasyConnection } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-types";
|
||||
|
||||
export const dnsMadeEasyInsertTxtRecord = async (
|
||||
connection: TDNSMadeEasyConnection,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = connection;
|
||||
|
||||
logger.info({ hostedZoneId, domain, value }, "Inserting TXT record for DNS Made Easy");
|
||||
try {
|
||||
await request.post(
|
||||
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(hostedZoneId)}/records`),
|
||||
{
|
||||
type: "TXT",
|
||||
name: domain,
|
||||
value,
|
||||
ttl: 60
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { error?: string[] | string })?.error?.[0] ||
|
||||
(error.response?.data as { error?: string[] | string })?.error ||
|
||||
error.message ||
|
||||
"Unknown error";
|
||||
|
||||
if (error.status === 400 && error.message.includes("already exists")) {
|
||||
logger.info({ domain, value }, `Record already exists for domain: ${domain} and value: ${value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(typeof errorMessage === "string" ? errorMessage : String(errorMessage));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const dnsMadeEasyDeleteTxtRecord = async (
|
||||
connection: TDNSMadeEasyConnection,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = connection;
|
||||
|
||||
logger.info({ hostedZoneId, domain, value }, "Deleting TXT record for DNS Made Easy");
|
||||
try {
|
||||
const dnsRecords = await listDNSMadeEasyRecords(connection, { zoneId: hostedZoneId, type: "TXT", name: domain });
|
||||
|
||||
let foundRecord = false;
|
||||
if (dnsRecords.length > 0) {
|
||||
const recordToDelete = dnsRecords.find(
|
||||
(record) => record.type === "TXT" && record.name === domain && record.value === value
|
||||
);
|
||||
|
||||
if (recordToDelete) {
|
||||
await request.delete(
|
||||
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(hostedZoneId)}/records/${recordToDelete.id}`),
|
||||
{
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
foundRecord = true;
|
||||
}
|
||||
}
|
||||
if (!foundRecord) {
|
||||
logger.warn({ hostedZoneId, domain, value }, "Record to delete not found");
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { error?: string[] | string })?.error?.[0] ||
|
||||
(error.response?.data as { error?: string[] | string })?.error ||
|
||||
error.message ||
|
||||
"Unknown error";
|
||||
throw new Error(typeof errorMessage === "string" ? errorMessage : String(errorMessage));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -655,7 +655,6 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
||||
name,
|
||||
projectId,
|
||||
configuration,
|
||||
enableDirectIssuance,
|
||||
actor,
|
||||
status
|
||||
}: {
|
||||
@@ -663,16 +662,8 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
||||
name: string;
|
||||
projectId: string;
|
||||
configuration: TCreateAzureAdCsCertificateAuthorityDTO["configuration"];
|
||||
enableDirectIssuance: boolean;
|
||||
actor: OrgServiceActor;
|
||||
}) => {
|
||||
// Azure ADCS does not support direct issuance - enforce this restriction
|
||||
if (enableDirectIssuance) {
|
||||
throw new BadRequestError({
|
||||
message: "Azure ADCS Certificate Authorities do not support direct issuance"
|
||||
});
|
||||
}
|
||||
|
||||
const { azureAdcsConnectionId } = configuration;
|
||||
const appConnection = await appConnectionDAL.findById(azureAdcsConnectionId);
|
||||
|
||||
@@ -737,24 +728,15 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
||||
id,
|
||||
status,
|
||||
configuration,
|
||||
enableDirectIssuance,
|
||||
actor,
|
||||
name
|
||||
}: {
|
||||
id: string;
|
||||
status?: CaStatus;
|
||||
configuration: TUpdateAzureAdCsCertificateAuthorityDTO["configuration"];
|
||||
enableDirectIssuance?: boolean;
|
||||
actor: OrgServiceActor;
|
||||
name?: string;
|
||||
}) => {
|
||||
// Azure ADCS does not support direct issuance - enforce this restriction
|
||||
if (enableDirectIssuance) {
|
||||
throw new BadRequestError({
|
||||
message: "Azure ADCS Certificate Authorities do not support direct issuance"
|
||||
});
|
||||
}
|
||||
|
||||
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
|
||||
if (configuration) {
|
||||
const { azureAdcsConnectionId } = configuration;
|
||||
@@ -795,13 +777,12 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (name || status || enableDirectIssuance !== undefined) {
|
||||
if (name || status) {
|
||||
await certificateAuthorityDAL.updateById(
|
||||
id,
|
||||
{
|
||||
name,
|
||||
status,
|
||||
enableDirectIssuance: false // Always false for Azure ADCS CAs
|
||||
status
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
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)
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ import { CaType } from "./certificate-authority-enums";
|
||||
import {
|
||||
TCertificateAuthority,
|
||||
TCreateCertificateAuthorityDTO,
|
||||
TDeprecatedUpdateCertificateAuthorityDTO,
|
||||
TUpdateCertificateAuthorityDTO
|
||||
} from "./certificate-authority-types";
|
||||
import { TExternalCertificateAuthorityDALFactory } from "./external-certificate-authority-dal";
|
||||
@@ -128,7 +129,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
const createCertificateAuthority = async (
|
||||
{ type, projectId, name, enableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO,
|
||||
{ type, projectId, name, configuration, status }: TCreateCertificateAuthorityDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
@@ -150,7 +151,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
...(configuration as TCreateInternalCertificateAuthorityDTO["configuration"]),
|
||||
isInternal: true,
|
||||
projectId,
|
||||
enableDirectIssuance,
|
||||
name
|
||||
});
|
||||
|
||||
@@ -176,7 +176,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
name,
|
||||
projectId,
|
||||
configuration: configuration as TCreateAcmeCertificateAuthorityDTO["configuration"],
|
||||
enableDirectIssuance,
|
||||
status,
|
||||
actor
|
||||
});
|
||||
@@ -187,7 +186,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
name,
|
||||
projectId,
|
||||
configuration: configuration as TCreateAzureAdCsCertificateAuthorityDTO["configuration"],
|
||||
enableDirectIssuance,
|
||||
status,
|
||||
actor
|
||||
});
|
||||
@@ -196,6 +194,63 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
const findCertificateAuthorityById = async ({ id, type }: { id: string; type: CaType }, actor: OrgServiceActor) => {
|
||||
const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id);
|
||||
|
||||
if (!certificateAuthority)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find certificate authority with id "${id}"`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: certificateAuthority.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
if (type === CaType.INTERNAL) {
|
||||
if (!certificateAuthority.internalCa?.id) {
|
||||
throw new NotFoundError({
|
||||
message: `Internal certificate authority with id "${id}" not found`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: certificateAuthority.id,
|
||||
type,
|
||||
enableDirectIssuance: certificateAuthority.enableDirectIssuance,
|
||||
name: certificateAuthority.name,
|
||||
projectId: certificateAuthority.projectId,
|
||||
configuration: certificateAuthority.internalCa,
|
||||
status: certificateAuthority.status
|
||||
} as TCertificateAuthority;
|
||||
}
|
||||
|
||||
if (certificateAuthority.externalCa?.type !== type) {
|
||||
throw new NotFoundError({
|
||||
message: `Could not find external certificate authority with id ${id} and type "${type}"`
|
||||
});
|
||||
}
|
||||
|
||||
if (type === CaType.ACME) {
|
||||
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
const findCertificateAuthorityByNameAndProjectId = async (
|
||||
{ caName, type, projectId }: { caName: string; type: CaType; projectId: string },
|
||||
actor: OrgServiceActor
|
||||
@@ -308,7 +363,145 @@ export const certificateAuthorityServiceFactory = ({
|
||||
};
|
||||
|
||||
const updateCertificateAuthority = async (
|
||||
{ caName, type, configuration, enableDirectIssuance, status, name, projectId }: TUpdateCertificateAuthorityDTO,
|
||||
{ id, type, configuration, status, name }: TUpdateCertificateAuthorityDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id);
|
||||
|
||||
if (!certificateAuthority)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find certificate authority with id "${id}"`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: certificateAuthority.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
if (type === CaType.INTERNAL) {
|
||||
if (!certificateAuthority.internalCa?.id) {
|
||||
throw new NotFoundError({
|
||||
message: `Internal certificate authority with id "${id}" not found`
|
||||
});
|
||||
}
|
||||
|
||||
const updatedCa = await internalCertificateAuthorityService.updateCaById({
|
||||
isInternal: true,
|
||||
caId: certificateAuthority.id,
|
||||
status,
|
||||
name
|
||||
});
|
||||
|
||||
if (!updatedCa.internalCa) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update internal certificate authority"
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: updatedCa.id,
|
||||
type,
|
||||
enableDirectIssuance: updatedCa.enableDirectIssuance,
|
||||
name: updatedCa.name,
|
||||
projectId: updatedCa.projectId,
|
||||
configuration: updatedCa.internalCa,
|
||||
status: updatedCa.status
|
||||
} as TCertificateAuthority;
|
||||
}
|
||||
|
||||
if (type === CaType.ACME) {
|
||||
return acmeFns.updateCertificateAuthority({
|
||||
id: certificateAuthority.id,
|
||||
configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"],
|
||||
actor,
|
||||
status,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return azureAdCsFns.updateCertificateAuthority({
|
||||
id: certificateAuthority.id,
|
||||
configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"],
|
||||
actor,
|
||||
status,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
const deleteCertificateAuthority = async ({ id, type }: { id: string; type: CaType }, actor: OrgServiceActor) => {
|
||||
const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id);
|
||||
|
||||
if (!certificateAuthority)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find certificate authority with id "${id}"`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: certificateAuthority.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) {
|
||||
throw new BadRequestError({
|
||||
message: "Internal certificate authority cannot be deleted"
|
||||
});
|
||||
}
|
||||
|
||||
if (certificateAuthority.externalCa?.id && certificateAuthority.externalCa.type !== type) {
|
||||
throw new BadRequestError({
|
||||
message: "External certificate authority cannot be deleted"
|
||||
});
|
||||
}
|
||||
|
||||
await certificateAuthorityDAL.deleteById(certificateAuthority.id);
|
||||
|
||||
if (type === CaType.INTERNAL) {
|
||||
return {
|
||||
id: certificateAuthority.id,
|
||||
type,
|
||||
enableDirectIssuance: certificateAuthority.enableDirectIssuance,
|
||||
name: certificateAuthority.name,
|
||||
projectId: certificateAuthority.projectId,
|
||||
configuration: certificateAuthority.internalCa,
|
||||
status: certificateAuthority.status
|
||||
} as TCertificateAuthority;
|
||||
}
|
||||
|
||||
if (type === CaType.ACME) {
|
||||
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
const deprecatedUpdateCertificateAuthority = async (
|
||||
{ caName, type, configuration, status, name, projectId }: TDeprecatedUpdateCertificateAuthorityDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const certificateAuthority = await certificateAuthorityDAL.findByNameAndProjectIdWithAssociatedCa(
|
||||
@@ -344,7 +537,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
const updatedCa = await internalCertificateAuthorityService.updateCaById({
|
||||
isInternal: true,
|
||||
enableDirectIssuance,
|
||||
caId: certificateAuthority.id,
|
||||
status,
|
||||
name
|
||||
@@ -371,7 +563,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return acmeFns.updateCertificateAuthority({
|
||||
id: certificateAuthority.id,
|
||||
configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"],
|
||||
enableDirectIssuance,
|
||||
actor,
|
||||
status,
|
||||
name
|
||||
@@ -382,7 +573,6 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return azureAdCsFns.updateCertificateAuthority({
|
||||
id: certificateAuthority.id,
|
||||
configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"],
|
||||
enableDirectIssuance,
|
||||
actor,
|
||||
status,
|
||||
name
|
||||
@@ -392,7 +582,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
const deleteCertificateAuthority = async (
|
||||
const deprecatedDeleteCertificateAuthority = async (
|
||||
{ caName, type, projectId }: { caName: string; type: CaType; projectId: string },
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
@@ -529,11 +719,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
return {
|
||||
createCertificateAuthority,
|
||||
findCertificateAuthorityByNameAndProjectId,
|
||||
findCertificateAuthorityById,
|
||||
listCertificateAuthoritiesByProjectId,
|
||||
findCertificateAuthorityByNameAndProjectId,
|
||||
updateCertificateAuthority,
|
||||
deleteCertificateAuthority,
|
||||
getAzureAdcsTemplates,
|
||||
getCaById
|
||||
getCaById,
|
||||
deprecatedUpdateCertificateAuthority,
|
||||
deprecatedDeleteCertificateAuthority
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,9 +19,14 @@ export type TCertificateAuthorityInput =
|
||||
| TAcmeCertificateAuthorityInput
|
||||
| TCreateAzureAdCsCertificateAuthorityDTO;
|
||||
|
||||
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id">;
|
||||
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id" | "enableDirectIssuance">;
|
||||
|
||||
export type TUpdateCertificateAuthorityDTO = Partial<Omit<TCreateCertificateAuthorityDTO, "projectId">> & {
|
||||
type: CaType;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TDeprecatedUpdateCertificateAuthorityDTO = Partial<Omit<TCreateCertificateAuthorityDTO, "projectId">> & {
|
||||
type: CaType;
|
||||
caName: 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 appCfg = getConfig();
|
||||
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
@@ -366,8 +366,8 @@ export const InternalCertificateAuthorityFns = ({
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const appCfg = getConfig();
|
||||
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "../certificate-authority-schemas";
|
||||
import { validateCaDateField } from "../certificate-authority-validators";
|
||||
|
||||
const InternalCertificateAuthorityConfigurationSchema = z
|
||||
export const InternalCertificateAuthorityConfigurationSchema = z
|
||||
.object({
|
||||
type: z.nativeEnum(InternalCaType).describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.type),
|
||||
friendlyName: z.string().optional().describe(CertificateAuthorities.CONFIGURATIONS.INTERNAL.friendlyName),
|
||||
|
||||
@@ -34,8 +34,6 @@ import {
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage,
|
||||
CertSignatureAlgorithm,
|
||||
CertSignatureType,
|
||||
CertStatus,
|
||||
TAltNameMapping
|
||||
} from "../../certificate/certificate-types";
|
||||
@@ -127,6 +125,22 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
kmsService,
|
||||
permissionService
|
||||
}: TInternalCertificateAuthorityServiceFactoryDep) => {
|
||||
const $checkSignature = (caKeyAlg: string, requestedKeyType: string, signatureAlgorithm?: string) => {
|
||||
const isRsaCa = caKeyAlg.startsWith("RSA");
|
||||
const isEcdsaCa = caKeyAlg.startsWith("EC") || caKeyAlg.startsWith("ECDSA");
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const caSupports = isRsaCa ? "RSA" : isEcdsaCa ? "ECDSA" : "unknown";
|
||||
|
||||
const isRequestValid = (requestedKeyType === "RSA" && isRsaCa) || (requestedKeyType === "ECDSA" && isEcdsaCa);
|
||||
|
||||
if (!isRequestValid) {
|
||||
throw new BadRequestError({
|
||||
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlg}. CA can only sign with ${caSupports}-based signature algorithms.`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createCa = async ({
|
||||
type,
|
||||
friendlyName,
|
||||
@@ -140,7 +154,6 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
notAfter,
|
||||
maxPathLength,
|
||||
keyAlgorithm,
|
||||
enableDirectIssuance,
|
||||
name,
|
||||
...dto
|
||||
}: TCreateCaDTO) => {
|
||||
@@ -192,9 +205,9 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
const ca = await certificateAuthorityDAL.create(
|
||||
{
|
||||
projectId,
|
||||
enableDirectIssuance,
|
||||
name: name || slugify(`${(friendlyName || dn).slice(0, 16)}-${alphaNumericNanoId(8)}`),
|
||||
status: type === InternalCaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE
|
||||
status: type === InternalCaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE,
|
||||
enableDirectIssuance: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -354,7 +367,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
* Update CA with id [caId].
|
||||
* Note: Used to enable/disable CA
|
||||
*/
|
||||
const updateCaById = async ({ caId, status, enableDirectIssuance, name, ...dto }: TUpdateCaDTO) => {
|
||||
const updateCaById = async ({ caId, status, name, ...dto }: TUpdateCaDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||
if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
|
||||
@@ -375,8 +388,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
}
|
||||
|
||||
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
|
||||
if (enableDirectIssuance !== undefined || status !== undefined || name !== undefined) {
|
||||
await certificateAuthorityDAL.updateById(ca.id, { enableDirectIssuance, status, name }, tx);
|
||||
if (status !== undefined || name !== undefined) {
|
||||
await certificateAuthorityDAL.updateById(ca.id, { status, name }, tx);
|
||||
}
|
||||
|
||||
return certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx);
|
||||
@@ -971,9 +984,9 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
const serialNumber = createSerialNumber();
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
|
||||
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const intermediateCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
@@ -1302,26 +1315,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]);
|
||||
|
||||
if (signatureAlgorithm) {
|
||||
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
|
||||
const requestedKeyType = signatureAlgorithm.split("-")[0];
|
||||
|
||||
const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]);
|
||||
const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]);
|
||||
|
||||
if (
|
||||
(requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) ||
|
||||
(requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa)
|
||||
) {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const supportedType = isRsaCa
|
||||
? CertSignatureAlgorithm.RSA_SHA256.split("-")[0]
|
||||
: isEcdsaCa
|
||||
? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0]
|
||||
: "unknown";
|
||||
throw new BadRequestError({
|
||||
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
|
||||
});
|
||||
}
|
||||
$checkSignature(ca.internalCa.keyAlgorithm, signatureAlgorithm.split("-")[0], signatureAlgorithm);
|
||||
}
|
||||
|
||||
// Determine signing algorithm for certificate signing
|
||||
@@ -1352,8 +1346,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const appCfg = getConfig();
|
||||
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
@@ -1690,22 +1684,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
}
|
||||
|
||||
if (signatureAlgorithm) {
|
||||
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
|
||||
const requestedKeyType = signatureAlgorithm.split("-")[0]; // Get the first part (RSA, ECDSA)
|
||||
|
||||
const isRsaCa = caKeyAlgorithm.startsWith(CertSignatureType.RSA);
|
||||
const isEcdsaCa = caKeyAlgorithm.startsWith(CertSignatureType.ECDSA);
|
||||
|
||||
if (
|
||||
(requestedKeyType === CertSignatureType.RSA && !isRsaCa) ||
|
||||
(requestedKeyType === CertSignatureType.ECDSA && !isEcdsaCa)
|
||||
) {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const supportedType = isRsaCa ? CertSignatureType.RSA : isEcdsaCa ? CertSignatureType.ECDSA : "unknown";
|
||||
throw new BadRequestError({
|
||||
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
|
||||
});
|
||||
}
|
||||
$checkSignature(ca.internalCa.keyAlgorithm, signatureAlgorithm.split("-")[0], signatureAlgorithm);
|
||||
}
|
||||
|
||||
const effectiveKeyAlgorithm = (keyAlgorithm || ca.internalCa.keyAlgorithm) as CertKeyAlgorithm;
|
||||
@@ -1728,9 +1707,9 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
|
||||
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
|
||||
@@ -48,7 +48,6 @@ export type TCreateCaDTO =
|
||||
notAfter?: string;
|
||||
maxPathLength?: number | null;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
enableDirectIssuance: boolean;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
@@ -66,7 +65,6 @@ export type TCreateCaDTO =
|
||||
notAfter?: string;
|
||||
maxPathLength?: number | null;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
enableDirectIssuance: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TGetCaDTO = {
|
||||
@@ -79,14 +77,12 @@ export type TUpdateCaDTO =
|
||||
caId: string;
|
||||
name?: string;
|
||||
status?: CaStatus;
|
||||
enableDirectIssuance?: boolean;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
caId: string;
|
||||
name?: string;
|
||||
status?: CaStatus;
|
||||
enableDirectIssuance?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TDeleteCaDTO = {
|
||||
|
||||
@@ -1279,7 +1279,7 @@ export const certificateV3ServiceFactory = ({
|
||||
status: CertificateOrderStatus.VALID
|
||||
})),
|
||||
authorizations: [],
|
||||
finalize: `/api/v3/pki/certificates/orders/${orderId}/completed`,
|
||||
finalize: `/api/v1/cert-manager/certificates/orders/${orderId}/completed`,
|
||||
certificate: certificateResult.certificate,
|
||||
projectId: certificateResult.projectId,
|
||||
profileName: certificateResult.profileName
|
||||
|
||||
@@ -52,7 +52,10 @@ import {
|
||||
} from "./certificate-types";
|
||||
|
||||
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">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
|
||||
@@ -91,8 +94,8 @@ export const certificateServiceFactory = ({
|
||||
/**
|
||||
* Return details for certificate with serial number [serialNumber]
|
||||
*/
|
||||
const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const getCert = async ({ id, serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
|
||||
const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@@ -117,13 +120,14 @@ export const certificateServiceFactory = ({
|
||||
* Get certificate private key.
|
||||
*/
|
||||
const getCertPrivateKey = async ({
|
||||
id,
|
||||
serialNumber,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetCertPrivateKeyDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@@ -156,8 +160,8 @@ export const certificateServiceFactory = ({
|
||||
/**
|
||||
* Delete certificate with serial number [serialNumber]
|
||||
*/
|
||||
const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const deleteCert = async ({ id, serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
|
||||
const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@@ -193,6 +197,7 @@ export const certificateServiceFactory = ({
|
||||
* of its issuing CA
|
||||
*/
|
||||
const revokeCert = async ({
|
||||
id,
|
||||
serialNumber,
|
||||
revocationReason,
|
||||
actorId,
|
||||
@@ -200,7 +205,7 @@ export const certificateServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TRevokeCertDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
|
||||
|
||||
if (!cert.caId) {
|
||||
throw new BadRequestError({
|
||||
@@ -290,8 +295,8 @@ export const certificateServiceFactory = ({
|
||||
* Return certificate body and certificate chain for certificate with
|
||||
* serial number [serialNumber]
|
||||
*/
|
||||
const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const getCertBody = async ({ id, serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => {
|
||||
const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@@ -584,8 +589,15 @@ export const certificateServiceFactory = ({
|
||||
* Return certificate body and certificate chain for certificate with
|
||||
* serial number [serialNumber]
|
||||
*/
|
||||
const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => {
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const getCertBundle = async ({
|
||||
id,
|
||||
serialNumber,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetCertBundleDTO) => {
|
||||
const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@@ -673,12 +685,13 @@ export const certificateServiceFactory = ({
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey,
|
||||
serialNumber,
|
||||
serialNumber: cert.serialNumber,
|
||||
cert
|
||||
};
|
||||
};
|
||||
|
||||
const getCertPkcs12 = async ({
|
||||
id,
|
||||
serialNumber,
|
||||
password,
|
||||
alias,
|
||||
@@ -700,7 +713,7 @@ export const certificateServiceFactory = ({
|
||||
if (!alias || alias.trim() === "") {
|
||||
throw new BadRequestError({ message: "Alias is required for PKCS12 keystore generation" });
|
||||
}
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const cert = id ? await certificateDAL.findById(id) : await certificateDAL.findOne({ serialNumber });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@@ -718,7 +731,7 @@ export const certificateServiceFactory = ({
|
||||
|
||||
// Get certificate bundle (certificate, chain, private key)
|
||||
const { certificate, certificateChain, privateKey } = await getCertBundle({
|
||||
serialNumber,
|
||||
id: cert.id,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
|
||||
@@ -84,20 +84,24 @@ export enum CrlReason {
|
||||
}
|
||||
|
||||
export type TGetCertDTO = {
|
||||
serialNumber: string;
|
||||
id?: string;
|
||||
serialNumber?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteCertDTO = {
|
||||
serialNumber: string;
|
||||
id?: string;
|
||||
serialNumber?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRevokeCertDTO = {
|
||||
serialNumber: string;
|
||||
id?: string;
|
||||
serialNumber?: string;
|
||||
revocationReason: CrlReason;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertBodyDTO = {
|
||||
serialNumber: string;
|
||||
id?: string;
|
||||
serialNumber?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TImportCertDTO = {
|
||||
@@ -112,15 +116,18 @@ export type TImportCertDTO = {
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertPrivateKeyDTO = {
|
||||
serialNumber: string;
|
||||
id?: string;
|
||||
serialNumber?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertBundleDTO = {
|
||||
serialNumber: string;
|
||||
id?: string;
|
||||
serialNumber?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertPkcs12DTO = {
|
||||
serialNumber: string;
|
||||
id?: string;
|
||||
serialNumber?: string;
|
||||
password: string;
|
||||
alias: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -270,7 +270,13 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
const tokenReviewerJwtSnippet = `${tokenReviewerJwt?.substring?.(0, 10) || ""}...${tokenReviewerJwt?.substring?.(tokenReviewerJwt.length - 10) || ""}`;
|
||||
const serviceAccountJwtSnippet = `${serviceAccountJwt?.substring?.(0, 10) || ""}...${serviceAccountJwt?.substring?.(serviceAccountJwt.length - 10) || ""}`;
|
||||
if (err instanceof AxiosError) {
|
||||
logger.error(
|
||||
{ response: err.response, host, port, tokenReviewerJwtSnippet, serviceAccountJwtSnippet },
|
||||
"tokenReviewCallbackRaw: Kubernetes token review request error (request error)"
|
||||
);
|
||||
if (err.response) {
|
||||
const { message } = err?.response?.data as unknown as { message?: string };
|
||||
|
||||
@@ -281,6 +287,11 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error(
|
||||
{ error: err as Error, host, port, tokenReviewerJwtSnippet, serviceAccountJwtSnippet },
|
||||
"tokenReviewCallbackRaw: Kubernetes token review request error (non-request error)"
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
@@ -99,13 +99,28 @@ export const identityOidcAuthServiceFactory = ({
|
||||
}
|
||||
|
||||
const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert });
|
||||
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
|
||||
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
|
||||
{
|
||||
httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
|
||||
}
|
||||
);
|
||||
|
||||
let discoveryDoc: { jwks_uri: string };
|
||||
try {
|
||||
const response = await axios.get<{ jwks_uri: string }>(
|
||||
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
|
||||
{
|
||||
httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
|
||||
}
|
||||
);
|
||||
discoveryDoc = response.data;
|
||||
} catch (error) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: Failed to fetch OIDC discovery document from ${identityOidcAuth.oidcDiscoveryUrl}. ${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
|
||||
const jwksUri = discoveryDoc.jwks_uri;
|
||||
if (!jwksUri) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: OIDC discovery document does not contain a jwks_uri. The identity provider may be misconfigured.`
|
||||
});
|
||||
}
|
||||
|
||||
const decodedToken = crypto.jwt().decode(oidcJwt, { complete: true });
|
||||
if (!decodedToken) {
|
||||
|
||||
@@ -105,7 +105,9 @@ export enum IntegrationUrls {
|
||||
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
|
||||
|
||||
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations",
|
||||
CHEF_API_URL = "https://api.chef.io"
|
||||
CHEF_API_URL = "https://api.chef.io",
|
||||
DNS_MADE_EASY_API_URL = "https://api.dnsmadeeasy.com",
|
||||
DNS_MADE_EASY_SANDBOX_API_URL = "https://api.sandbox.dnsmadeeasy.com"
|
||||
}
|
||||
|
||||
export const getIntegrationOptions = async () => {
|
||||
|
||||
@@ -94,6 +94,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
|
||||
db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"),
|
||||
|
||||
db.ref("slug").withSchema(TableName.Role).as("roleSlug"),
|
||||
db.ref("name").withSchema(TableName.Role).as("roleName"),
|
||||
db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"),
|
||||
db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"),
|
||||
db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"),
|
||||
@@ -180,6 +181,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
|
||||
label: "roles" as const,
|
||||
mapper: ({
|
||||
roleSlug,
|
||||
roleName,
|
||||
membershipRoleId,
|
||||
membershipRole,
|
||||
membershipRoleIsTemporary,
|
||||
@@ -193,6 +195,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
|
||||
id: membershipRoleId,
|
||||
role: membershipRole,
|
||||
customRoleSlug: roleSlug,
|
||||
customRoleName: roleName,
|
||||
temporaryRange: membershipRoleTemporaryRange,
|
||||
temporaryMode: membershipRoleTemporaryMode,
|
||||
temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime,
|
||||
|
||||
@@ -524,8 +524,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
|
||||
@@ -466,8 +466,8 @@ export const pkiTemplatesServiceFactory = ({
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/cert-manager/ca/internal/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TAuditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||
import { TScimServiceFactory } from "@app/ee/services/scim/scim-types";
|
||||
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||
import { TKeyValueStoreDALFactory } from "@app/keystore/key-value-store-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -29,6 +30,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
orgService: TOrgServiceFactory;
|
||||
userNotificationDAL: Pick<TUserNotificationDALFactory, "pruneNotifications">;
|
||||
keyValueStoreDAL: Pick<TKeyValueStoreDALFactory, "pruneExpiredKeys">;
|
||||
scimService: Pick<TScimServiceFactory, "notifyExpiringTokens">;
|
||||
};
|
||||
|
||||
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
|
||||
@@ -44,6 +46,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
secretVersionV2DAL,
|
||||
identityUniversalAuthClientSecretDAL,
|
||||
serviceTokenService,
|
||||
scimService,
|
||||
orgService,
|
||||
userNotificationDAL,
|
||||
keyValueStoreDAL
|
||||
@@ -86,6 +89,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
await secretVersionV2DAL.pruneExcessVersions();
|
||||
await secretFolderVersionDAL.pruneExcessVersions();
|
||||
await serviceTokenService.notifyExpiringTokens();
|
||||
await scimService.notifyExpiringTokens();
|
||||
await orgService.notifyInvitedUsers();
|
||||
await auditLogDAL.pruneAuditLog();
|
||||
await userNotificationDAL.pruneNotifications();
|
||||
|
||||
@@ -421,11 +421,12 @@ export const fnSecretBulkDelete = async ({
|
||||
);
|
||||
|
||||
const changes = deletedSecrets
|
||||
.filter(({ type }) => type === SecretType.Shared)
|
||||
.filter(({ type, id }) => type === SecretType.Shared && secretVersions[id])
|
||||
.map(({ id }) => ({
|
||||
type: CommitType.DELETE,
|
||||
secretVersionId: secretVersions[id].id
|
||||
secretVersionId: secretVersions[id]?.id
|
||||
}));
|
||||
|
||||
if (changes.length > 0) {
|
||||
if (commitChanges) {
|
||||
commitChanges.push(...changes);
|
||||
|
||||
@@ -2254,7 +2254,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
]
|
||||
}
|
||||
});
|
||||
if (secretsToDelete.length !== inputSecrets.length)
|
||||
const secretsToDeleteSet = new Set(secretsToDelete.map((el) => el.key));
|
||||
if (secretsToDeleteSet.size !== inputSecrets.length)
|
||||
throw new NotFoundError({
|
||||
message: `One or more secrets does not exist: ${secretsToDelete.map((el) => el.key).join(", ")}`
|
||||
});
|
||||
|
||||
@@ -64,6 +64,8 @@ import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret
|
||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TTelemetryServiceFactory } from "../telemetry/telemetry-service";
|
||||
import { PostHogEventTypes } from "../telemetry/telemetry-types";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
||||
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
||||
@@ -120,6 +122,7 @@ type TSecretQueueFactoryDep = {
|
||||
reminderService: Pick<TReminderServiceFactory, "createReminderInternal" | "deleteReminderBySecretId">;
|
||||
eventBusService: TEventBusService;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
|
||||
};
|
||||
|
||||
export type TGetSecrets = {
|
||||
@@ -184,7 +187,8 @@ export const secretQueueFactory = ({
|
||||
eventBusService,
|
||||
licenseService,
|
||||
membershipUserDAL,
|
||||
membershipRoleDAL
|
||||
membershipRoleDAL,
|
||||
telemetryService
|
||||
}: TSecretQueueFactoryDep) => {
|
||||
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
||||
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
||||
@@ -1029,6 +1033,29 @@ export const secretQueueFactory = ({
|
||||
isSynced: response?.isSynced ?? true
|
||||
});
|
||||
|
||||
await telemetryService.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IntegrationSynced,
|
||||
distinctId: `project/${projectId}`,
|
||||
organizationId: project.orgId,
|
||||
properties: {
|
||||
integrationId: integration.id,
|
||||
integration: integration.integration,
|
||||
environment,
|
||||
secretPath,
|
||||
projectId,
|
||||
url: integration.url ?? undefined,
|
||||
app: integration.app ?? undefined,
|
||||
appId: integration.appId ?? undefined,
|
||||
targetEnvironment: integration.targetEnvironment ?? undefined,
|
||||
targetEnvironmentId: integration.targetEnvironmentId ?? undefined,
|
||||
targetService: integration.targetService ?? undefined,
|
||||
targetServiceId: integration.targetServiceId ?? undefined,
|
||||
path: integration.path ?? undefined,
|
||||
region: integration.region ?? undefined,
|
||||
isManualSync: isManual ?? false
|
||||
}
|
||||
});
|
||||
|
||||
// May be undefined, if it's undefined we assume the sync was successful, hence the strict equality type check.
|
||||
if (response?.isSynced === false) {
|
||||
integrationsFailedToSync.push({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user