Merge branch 'main' into feature/ldap-gateway-support

This commit is contained in:
Victor Santos
2025-11-17 09:24:46 -03:00
391 changed files with 14651 additions and 4589 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Keys # Keys
# Required key for platform encryption/decryption ops # Required key for platform encryption/decryption ops
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION # THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 ENCRYPTION_KEY=VVHnGZ0w98WLgISK4XSJcagezuG6EWRFTk48KE4Y5Mw=
# JWT # JWT
# Required secrets to sign JWT tokens # Required secrets to sign JWT tokens
+1 -1
View File
@@ -1,2 +1,2 @@
DB_CONNECTION_URI= DB_CONNECTION_URI=postgres://infisical:infisical@localhost:5432/infisical
AUDIT_LOGS_DB_CONNECTION_URI= AUDIT_LOGS_DB_CONNECTION_URI=
-2
View File
@@ -21,5 +21,3 @@
--- ---
- [ ] 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). 📝 - [ ] 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). 📝
<!-- If you have any questions regarding contribution, here's the FAQ : https://infisical.com/docs/contributing/getting-started/faq -->
+109
View File
@@ -0,0 +1,109 @@
name: "Run backend BDD tests"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/**"
- "!backend/README.md"
- "!backend/.*"
- "backend/.eslintrc.js"
workflow_call:
jobs:
run-backend-bdd-tests:
name: Run BDD tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Free up disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
docker system prune -af
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Install Python
run: uv python install
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker compose` for local simulations
with:
version: "2.14.2"
- name: 🔧 Setup Node 20
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
cache-dependency-path: backend/package-lock.json
- name: Install dependencies
run: npm install
working-directory: backend
- name: Output .env file and enable feature flags for BDD tests
run: |
cp .env.example .env
echo "ACME_DEVELOPMENT_MODE=true" >> .env
echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\", \"infisical.com\": \"host.docker.internal:8087\", \"example.com\": \"host.docker.internal:8087\"}" >> .env
echo "BDD_NOCK_API_ENABLED=true" >> .env
# Skip upstream validation, otherwise the ACME client for the upstream will try to
# validate the DNS records, which will fail because the DNS records are not actually created.
echo "ACME_SKIP_UPSTREAM_VALIDATION=true" >> .env
# We are not using FIPS mode, need a different encryption key for BDD tests
NEW_ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
sed -i "s#ENCRYPTION_KEY=.*#ENCRYPTION_KEY=$NEW_ENCRYPTION_KEY#" .env
# Enable ACME feature in license for BDD tests
sed -i 's/pkiAcme: .*/pkiAcme: true,/g' backend/src/ee/services/license/license-fns.ts
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=moby/buildkit:latest
- name: Build Infisical backend Docker image with caching
uses: docker/bake-action@v5
timeout-minutes: 30
with:
files: docker-compose.bdd.yml
targets: backend
load: true
# Uncomment this to force a rebuild of the image
# no-cache: true
set: |
*.cache-from=type=gha,scope=infisical-backend-bdd-tests
*.cache-to=type=gha,mode=max,scope=infisical-backend-bdd-tests
- name: Start Infisical
run: docker compose -f docker-compose.bdd.yml up -d
- name: Wait for API to be ready
uses: nick-fields/retry@v3
with:
timeout_seconds: 60
max_attempts: 30
command: |
curl -f -X GET http://localhost:8080/api/v1/admin/config
- name: Run bdd tests
run: npm run test:bdd
working-directory: backend
env:
INFISICAL_API_URL: http://localhost:8080
BOOTSTRAP_INFISICAL: "1"
- name: cleanup
run: |
docker compose -f "docker-compose.bdd.yml" down
- name: Dump backend logs
if: always() # Ensures this runs even if previous steps fail
run: |
mkdir -p logs
docker compose -f docker-compose.bdd.yml logs backend > logs/backend.log 2>&1 || true
- name: Upload backend logs as artifact
if: always() # Always upload, even on failure/cancellation
uses: actions/upload-artifact@v4
with:
name: backend-logs-${{ github.run_id }}
path: logs/backend.log
retention-days: 7
if-no-files-found: warn
+1
View File
@@ -71,5 +71,6 @@ frontend-build
cli/infisical-merge cli/infisical-merge
cli/test/infisical-merge cli/test/infisical-merge
/backend/binary /backend/binary
backend/bdd/.bdd-infisical-bootstrap-result.json
/npm/bin /npm/bin
+1 -1
View File
@@ -87,7 +87,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus
## Getting started ## Getting started
Check out the [Quickstart Guides](https://infisical.com/docs/getting-started/introduction) Check out the [Quickstart Guides](https://infisical.com/docs/documentation/getting-started/overview)
| Use Infisical Cloud | Deploy Infisical on premise | | Use Infisical Cloud | Deploy Infisical on premise |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
+198 -10
View File
@@ -1,26 +1,214 @@
import json
import os import os
import pathlib
import typing
import httpx import httpx
from behave.runner import Context from behave.runner import Context
from dotenv import load_dotenv from dotenv import load_dotenv
from faker import Faker
import logging
from features.steps.utils import clean_all_nock, restore_nock
load_dotenv() load_dotenv()
logger = logging.getLogger(__name__)
BASE_URL = os.environ.get("INFISICAL_API_URL", "http://localhost:8080") BASE_URL = os.environ.get("INFISICAL_API_URL", "http://localhost:8080")
PEBBLE_URL = os.environ.get("PEBBLE_URL", "https://pebble:14000/dir")
PROJECT_ID = os.environ.get("PROJECT_ID") PROJECT_ID = os.environ.get("PROJECT_ID")
CERT_CA_ID = os.environ.get("CERT_CA_ID") CERT_CA_ID = os.environ.get("CERT_CA_ID")
CERT_TEMPLATE_ID = os.environ.get("CERT_TEMPLATE_ID") CERT_TEMPLATE_ID = os.environ.get("CERT_TEMPLATE_ID")
AUTH_TOKEN = os.environ.get("INFISICAL_TOKEN") AUTH_TOKEN = os.environ.get("INFISICAL_TOKEN")
BOOTSTRAP_INFISICAL = int(os.environ.get("BOOTSTRAP_INFISICAL", 0))
# Called mostly from a CI to setup the new Infisical instance to get it ready for BDD tests
def bootstrap_infisical(context: Context):
bootstrap_result_file = pathlib.Path.cwd() / ".bdd-infisical-bootstrap-result.json"
if bootstrap_result_file.exists():
logger.info(
"Bootstrap result file exists at %s, loading it now", bootstrap_result_file
)
return json.loads(bootstrap_result_file.read_text())
faker = Faker()
with httpx.Client(base_url=BASE_URL) as client:
resp = client.post(
"/api/v1/admin/signup",
json={
"email": f"{faker.user_name()}@infisical.com",
"password": faker.password(),
"firstName": faker.first_name(),
"lastName": faker.last_name(),
},
)
resp.raise_for_status()
body = resp.json()
org = body["organization"]
user = body["user"]
temp_token = body["token"]
resp = client.post(
"/api/v3/auth/select-organization",
headers={"Authorization": f"Bearer {temp_token}"},
json={"organizationId": org["id"]},
)
resp.raise_for_status()
body = resp.json()
temp_token = body["token"]
resp = client.post(
"/api/v1/auth/token",
headers={"Authorization": f"Bearer {temp_token}"},
json={},
)
resp.raise_for_status()
body = resp.json()
auth_token = body["token"]
headers = dict(authorization=f"Bearer {auth_token}")
project_slug = faker.slug()
resp = client.post(
"/api/v1/projects",
headers=headers,
json={
"projectName": project_slug,
"projectDescription": faker.paragraph(),
"template": "default",
"type": "cert-manager",
},
)
resp.raise_for_status()
body = resp.json()
project = body["project"]
ca_slug = faker.slug()
resp = client.post(
"/api/v1/pki/ca/internal",
headers=headers,
json={
"projectId": project["id"],
"name": ca_slug,
"type": "internal",
"status": "active",
"enableDirectIssuance": True,
"configuration": {
"type": "root",
"organization": "Infisican Inc",
"ou": "",
"country": "",
"province": "",
"locality": "",
"commonName": "",
"notAfter": "2035-11-07",
"maxPathLength": -1,
"keyAlgorithm": "RSA_2048",
},
},
)
resp.raise_for_status()
body = resp.json()
ca = body
cert_template_slug = faker.slug()
resp = client.post(
"/api/v2/certificate-templates",
headers=headers,
json={
"projectId": project["id"],
"name": cert_template_slug,
"description": "",
"subject": [{"type": "common_name", "allowed": ["*"]}],
"sans": [{"type": "dns_name", "allowed": ["*"]}],
"keyUsages": {
"required": [],
"allowed": [
"digital_signature",
"non_repudiation",
"key_encipherment",
"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"},
},
)
resp.raise_for_status()
body = resp.json()
cert_template = body["certificateTemplate"]
bootstrap_result = dict(
org=org,
user=user,
project=project,
ca=ca,
cert_template=cert_template,
auth_token=auth_token,
)
bootstrap_result_file.write_text(json.dumps(bootstrap_result))
return bootstrap_result
def before_all(context: Context): def before_all(context: Context):
context.vars = { if BOOTSTRAP_INFISICAL:
"BASE_URL": BASE_URL, details = bootstrap_infisical(context)
"PROJECT_ID": PROJECT_ID, context.vars = {
"CERT_CA_ID": CERT_CA_ID, "BASE_URL": BASE_URL,
"CERT_TEMPLATE_ID": CERT_TEMPLATE_ID, "PEBBLE_URL": PEBBLE_URL,
"AUTH_TOKEN": AUTH_TOKEN, "PROJECT_ID": details["project"]["id"],
} "CERT_CA_ID": details["ca"]["id"],
context.http_client = httpx.Client( "CERT_TEMPLATE_ID": details["cert_template"]["id"],
base_url=BASE_URL, # headers={"Authorization": f"Bearer {AUTH_TOKEN}"} "AUTH_TOKEN": details["auth_token"],
) }
else:
context.vars = {
"BASE_URL": BASE_URL,
"PEBBLE_URL": PEBBLE_URL,
"PROJECT_ID": PROJECT_ID,
"CERT_CA_ID": CERT_CA_ID,
"CERT_TEMPLATE_ID": CERT_TEMPLATE_ID,
"AUTH_TOKEN": AUTH_TOKEN,
}
context.http_client = httpx.Client(base_url=BASE_URL)
def after_scenario(context: Context, scenario: typing.Any):
if hasattr(context, "web_server"):
context.web_server.shutdown_and_server_close()
clean_all_nock(context)
restore_nock(context)
@@ -0,0 +1,273 @@
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"
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
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
Then I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then I peak and memorize the next nonce as nonce
Then I memorize <src_var> with jq "<jq>" as <dest_var>
When I send a raw ACME request to "<url>"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "<url>",
"kid": "{acme_account0.uri}"
},
"payload": {"invalid": "payload"}
}
"""
# With original owner account, the invalid payload is going to trigger other errors instead of 404, this is to make sure
# that our URLs are actually correct
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"
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>"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "<url>",
"kid": "{acme_account1.uri}"
},
"raw_payload": "<payload>"
}
"""
Then the value response.status_code should be equal to 404
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 | {order.uri} | |
| order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} |
| 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: 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"
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
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
Then I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then I peak and memorize the next nonce as nonce
Then I memorize <src_var> with jq "<jq>" as <dest_var>
When I send a raw ACME request to "<url>"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "<url>",
"kid": "{acme_account0.uri}"
},
"payload": {"invalid": "payload"}
}
"""
# With original owner account under their profile, the invalid payload is going to trigger other errors instead of
# 404, this is to make sure that our URLs are actually correct
Then the value response.status_code should not be equal to 404
And I put away current ACME client as client0
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
"""
{
"projectId": "{PROJECT_ID}",
"slug": "{profile_slug}",
"description": "",
"enrollmentType": "acme",
"caId": "{CERT_CA_ID}",
"certificateTemplateId": "{CERT_TEMPLATE_ID}",
"acmeConfig": {}
}
"""
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"
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"
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>
When I send a raw ACME request to "<url>"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "<url>",
"kid": "{acme_account1.uri}"
},
"payload": {}
}
"""
Then the value response.status_code should be equal to 404
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 | {order.uri} | |
| order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} |
| 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: 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"
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
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
Then I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then I peak and memorize the next nonce as nonce
Then I memorize <src_var> with jq "<jq>" as <dest_var>
When I send a raw ACME request to "<url>"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "<url>",
"kid": "{acme_account0.uri}"
},
"payload": {"invalid": "payload"}
}
"""
# With original owner account under their profile, the invalid payload is going to trigger other errors instead of
# 404, this is to make sure that our URLs are actually correct
Then the value response.status_code should not be equal to 404
And I put away current ACME client as client0
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
"""
{
"projectId": "{PROJECT_ID}",
"slug": "{profile_slug}",
"description": "",
"enrollmentType": "acme",
"caId": "{CERT_CA_ID}",
"certificateTemplateId": "{CERT_TEMPLATE_ID}",
"acmeConfig": {}
}
"""
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"
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
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>
When I send a raw ACME request to "<url>"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "<url>",
"kid": "{acme_account1.uri}"
},
"raw_payload": "<payload>"
}
"""
Then the value response.status_code should be equal to 404
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 | {order.uri} | |
| order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} |
| 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: 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"
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
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
Then I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then I peak and memorize the next nonce as nonce
Then I memorize <src_var> with jq "<jq>" as <dest_var>
When I send a raw ACME request to "<actual_url>"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "<bad_url>",
"kid": "{acme_account.uri}"
},
"payload": {}
}
"""
Then the value response.status_code should be equal to 400
Then the value response with jq ".status" should be equal to 400
Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:malformed"
Then the value response with jq ".detail" should be equal to "<error_detail>"
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 | {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 |
| order | . | not_used | {order.uri}/finalize | https://example.com/acmes/orders/FOOBAR/finalize | URL mismatch in the protected header |
| order | . | not_used | {order.uri}/certificate | BAD | Invalid URL in the protected header |
| order | . | not_used | {order.uri}/certificate | https://example.com/acmes/orders/FOOBAR/certificate | URL mismatch in the protected header |
| order | .authorizations[0].uri | auth_uri | {auth_uri} | BAD | Invalid URL in the protected header |
| order | .authorizations[0].uri | auth_uri | {auth_uri} | https://example.com/acmes/auths/FOOBAR | URL mismatch in the protected header |
| order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | BAD | Invalid URL in the protected header |
| order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | https://example.com/acmes/challenges/FOOBAR | URL mismatch in the protected header |
+49 -1
View File
@@ -2,5 +2,53 @@ Feature: Account
Scenario: Create a new account Scenario: Create a new account
Given I have an ACME cert profile as "acme_profile" Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
And the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/(.+)
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"
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 acme_account
And the value acme_account.uri should be equal to "{account_uri}"
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"
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"
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>"
Examples: Bad Credentials
| eab_kid | eab_secret | error_type | error_msg |
| bad | Cg== | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature |
| {acme_profile.eab_kid} | Cg== | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature |
| {acme_profile.eab_kid} | YmFkLXNjcmV0Cg== | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature |
| {acme_profile.eab_kid} | ABC{acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature |
| bad | {acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | External account binding KID mismatch |
| 4bc7959c-fe2d-4447-ae91-0cd893667af6 | {acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | External account binding KID mismatch |
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"
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 |
+8 -9
View File
@@ -2,8 +2,7 @@ Feature: Authorization
Scenario: Get authorization Scenario: Get authorization
Given I have an ACME cert profile as "acme_profile" Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
# # TODO: make it I have an account already instead?
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -13,11 +12,11 @@ Feature: Authorization
} }
""" """
Then I create a RSA private key pair as cert_key Then I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then 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/pki/acme/profiles/{acme_profile.id}/authorizations/(.+)
Then the value order.authorizations[0].body with jq ".status" should be equal to "pending" And the value order.authorizations[0].body with jq ".status" should be equal to "pending"
Then the value order.authorizations[0].body with jq ".challenges | map(pick(.type, .status)) | sort_by(.type)" should be equal to json And the value order.authorizations[0].body with jq ".challenges | map(pick(.type, .status)) | sort_by(.type)" should be equal to json
""" """
[ [
{ {
@@ -26,8 +25,8 @@ Feature: Authorization
} }
] ]
""" """
Then the value order.authorizations[0].body with jq ".challenges | map(.status) | sort" should be equal to ["pending"] And the value order.authorizations[0].body with jq ".challenges | map(.status) | sort" should be equal to ["pending"]
Then the value order.authorizations[0].body with jq ".identifier" should be equal to json And the value order.authorizations[0].body with jq ".identifier" should be equal to json
""" """
{ {
"type": "dns", "type": "dns",
@@ -2,8 +2,8 @@ Feature: ACME Cert Profile
Scenario: Create a cert profile Scenario: Create a cert profile
Given I make a random slug as profile_slug Given I make a random slug as profile_slug
Given I use AUTH_TOKEN for authentication And I use AUTH_TOKEN for authentication
When I send a POST request to "/api/v1/pki/certificate-profiles" with JSON payload When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload
""" """
{ {
"projectId": "{PROJECT_ID}", "projectId": "{PROJECT_ID}",
@@ -16,16 +16,16 @@ Feature: ACME Cert Profile
} }
""" """
Then the value response.status_code should be equal to 200 Then the value response.status_code should be equal to 200
Then the value response with jq ".certificateProfile.id" should be present And the value response with jq ".certificateProfile.id" should be present
Then the value response with jq ".certificateProfile.slug" should be equal to "{profile_slug}" And the value response with jq ".certificateProfile.slug" should be equal to "{profile_slug}"
Then the value response with jq ".certificateProfile.caId" should be equal to "{CERT_CA_ID}" And the value response with jq ".certificateProfile.caId" should be equal to "{CERT_CA_ID}"
Then the value response with jq ".certificateProfile.certificateTemplateId" should be equal to "{CERT_TEMPLATE_ID}" And the value response with jq ".certificateProfile.certificateTemplateId" should be equal to "{CERT_TEMPLATE_ID}"
Then the value response with jq ".certificateProfile.enrollmentType" should be equal to "acme" And the value response with jq ".certificateProfile.enrollmentType" should be equal to "acme"
Scenario: Reveal EAB secret Scenario: Reveal EAB secret
Given I make a random slug as profile_slug Given I make a random slug as profile_slug
Given I use AUTH_TOKEN for authentication And I use AUTH_TOKEN for authentication
When I send a POST request to "/api/v1/pki/certificate-profiles" with JSON payload When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload
""" """
{ {
"projectId": "{PROJECT_ID}", "projectId": "{PROJECT_ID}",
@@ -39,11 +39,11 @@ Feature: ACME Cert Profile
""" """
Then the value response.status_code should be equal to 200 Then the value response.status_code should be equal to 200
And I memorize response with jq ".certificateProfile.id" as profile_id And I memorize response with jq ".certificateProfile.id" as profile_id
When I send a GET request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal"
Then the value response.status_code should be equal to 200 Then the value response.status_code should be equal to 200
Then the value response with jq ".eabKid" should be equal to "{profile_id}" And the value response with jq ".eabKid" should be equal to "{profile_id}"
Then the value response with jq ".eabSecret" should be present And the value response with jq ".eabSecret" should be present
And I memorize response with jq ".eabKid" as eab_kid And I memorize response with jq ".eabKid" as eab_kid
And I memorize response with jq ".eabSecret" as eab_secret And I memorize response with jq ".eabSecret" as eab_secret
When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account
+196 -11
View File
@@ -2,8 +2,7 @@ Feature: Challenge
Scenario: Validate challenge Scenario: Validate challenge
Given I have an ACME cert profile as "acme_profile" Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
# # TODO: make it I have an account already instead?
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -12,12 +11,198 @@ Feature: Challenge
"COMMON_NAME": "localhost" "COMMON_NAME": "localhost"
} }
""" """
Then I create a RSA private key pair as cert_key And I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then I select challenge with type http-01 for domain localhost from order at order as challenge And I select challenge with type http-01 for domain localhost from order in order as challenge
Then I serve challenge response for challenge at localhost And I serve challenge response for challenge at localhost
Then I tell ACME server that challenge is ready to be verified And I tell ACME server that challenge is ready to be verified
Then I poll and finalize the ACME order order as finalized_order And I poll and finalize the ACME order order as finalized_order
Then the value finalized_order.body with jq ".status" should be equal to "valid" And the value finalized_order.body with jq ".status" should be equal to "valid"
# TODO: check the fullchain pem content of the order And I parse the full-chain certificate from order finalized_order as cert
And the value cert with jq ".subject.common_name" should be equal to "localhost"
Scenario: Validate challenges for multiple domains
Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
And I add subject alternative name to certificate signing request csr
"""
[
"infisical.com",
"example.com"
]
"""
And I create a RSA private key pair as cert_key
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
And I pass all challenges with type http-01 for order in order
And I poll and finalize the ACME order order as finalized_order
And the value finalized_order.body with jq ".status" should be equal to "valid"
And I parse the full-chain certificate from order finalized_order as cert
And the value cert with jq ".subject.common_name" should be equal to "localhost"
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
"""
[
"example.com",
"infisical.com"
]
"""
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"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
And I add subject alternative name to certificate signing request csr
"""
[
"infisical.com"
]
"""
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
# the localhost auth should be valid
And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "localhost")) | first | .uri" as localhost_auth
And I peak and memorize the next nonce as nonce
When I send a raw ACME request to "{localhost_auth}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{localhost_auth}",
"kid": "{acme_account.uri}"
}
}
"""
Then the value response.status_code should be equal to 200
And the value response with jq ".status" should be equal to "valid"
# the infisical.com auth should still be pending
And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "infisical.com")) | first | .uri" as infisical_auth
And I memorize response.headers with jq ".["replay-nonce"]" as nonce
When I send a raw ACME request to "{infisical_auth}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{infisical_auth}",
"kid": "{acme_account.uri}"
}
}
"""
Then the value response.status_code should be equal to 200
And the value response with jq ".status" should be equal to "pending"
# the order should be pending as well
And I memorize response.headers with jq ".["replay-nonce"]" as nonce
When I send a raw ACME request to "{order.uri}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{order.uri}",
"kid": "{acme_account.uri}"
}
}
"""
Then the value response.status_code should be equal to 200
And the value response with jq ".status" should be equal to "pending"
# finalize should not be allowed when all auths are not valid yet
And I memorize response.headers with jq ".["replay-nonce"]" as nonce
When I send a raw ACME request to "{order.body.finalize}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{order.body.finalize}",
"kid": "{acme_account.uri}"
},
"payload": {
"csr": "{csr_pem}"
}
}
"""
Then the value response.status_code should be equal to 400
Then the value response with jq ".status" should be equal to 400
Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:orderNotReady"
Then the value response with jq ".detail" should be equal to "ACME order is not ready"
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"
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": "example.com"
}
"""
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"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order",
"kid": "{acme_account.uri}"
},
"payload": {
"identifiers": [
{ "type": "dns", "value": "localhost" },
{ "type": "dns", "value": "infisical.com" }
]
}
}
"""
Then the value response.status_code should be equal to 201
And I memorize response with jq ".finalize" as finalize_url
And I memorize response.headers with jq ".["replay-nonce"]" as nonce
And I memorize response as order
And I pass all challenges with type http-01 for order in order
And I encode CSR csr_pem as JOSE Base-64 DER as base64_csr_der
When I send a raw ACME request to "{finalize_url}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{finalize_url}",
"kid": "{acme_account.uri}"
},
"payload": {
"csr": "{base64_csr_der}"
}
}
"""
Then the value response.status_code should be equal to 400
And the value response with jq ".status" should be equal to 400
And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badCSR"
And the value response with jq ".detail" should be equal to "Invalid CSR: Common name + SANs mismatch with order identifiers"
@@ -2,13 +2,13 @@ Feature: Directory
Scenario: Get the directory of ACME service urls Scenario: Get the directory of ACME service urls
Given I have an ACME cert profile as "acme_profile" Given I have an ACME cert profile as "acme_profile"
When I send a GET request to "/api/v1/pki/acme/profiles/{acme_profile.id}/directory" When I send a "GET" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
Then the response status code should be "200" Then the response status code should be "200"
Then the response body should match JSON value And the response body should match JSON value
""" """
{ {
"newNonce": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce", "newNonce": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce",
"newAccount": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account", "newAccount": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account",
"newOrder": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" "newOrder": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order"
} }
""" """
@@ -0,0 +1,180 @@
Feature: External CA
Scenario: Issue a certificate from an external CA
Given I create a Cloudflare connection as cloudflare
Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id
Given I create a external ACME CA with the following config as ext_ca
"""
{
"dnsProviderConfig": {
"provider": "cloudflare",
"hostedZoneId": "MOCK_ZONE_ID"
},
"directoryUrl": "{PEBBLE_URL}",
"accountEmail": "fangpen@infisical.com",
"dnsAppConnectionId": "{app_conn_id}",
"eabKid": "",
"eabHmacKey": ""
}
"""
Then I memorize ext_ca with jq ".id" as ext_ca_id
Given I create a certificate template with the following config as cert_template
"""
{
"subject": [
{
"type": "common_name",
"allowed": [
"*"
]
}
],
"sans": [
{
"type": "dns_name",
"allowed": [
"*"
]
}
],
"keyUsages": {
"required": [],
"allowed": [
"digital_signature",
"key_encipherment",
"non_repudiation",
"data_encipherment",
"key_agreement",
"key_cert_sign",
"crl_sign",
"encipher_only",
"decipher_only"
]
},
"extendedKeyUsages": {
"required": [],
"allowed": [
"client_auth",
"server_auth",
"code_signing",
"email_protection",
"ocsp_signing",
"time_stamping"
]
},
"algorithms": {
"signature": [
"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/pki/acme/profiles/{acme_profile.id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
# Pebble has a strict rule to only takes SANs
Then I add subject alternative name to certificate signing request csr
"""
[
"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.cloudflare.com:443",
"method": "POST",
"path": "/client/v4/zones/MOCK_ZONE_ID/dns_records",
"status": 200,
"response": {
"result": {
"id": "A2A6347F-88B5-442D-9798-95E408BC7701",
"name": "Mock Account",
"type": "standard",
"settings": {
"enforce_twofactor": false,
"api_access_enabled": null,
"access_approval_expiry": null,
"abuse_contact_email": null,
"user_groups_ui_beta": false
},
"legacy_flags": {
"enterprise_zone_quota": {
"maximum": 0,
"current": 0,
"available": 0
}
},
"created_on": "2013-04-18T00:41:02.215243Z"
},
"success": true,
"errors": [],
"messages": []
},
"responseIsBinary": false
},
{
"scope": "https://api.cloudflare.com:443",
"method": "GET",
"path": {
"regex": "/client/v4/zones/[^/]+/dns_records\\?"
},
"status": 200,
"response": {
"result": [],
"success": true,
"errors": [],
"messages": [],
"result_info": {
"page": 1,
"per_page": 100,
"count": 0,
"total_count": 0,
"total_pages": 1
}
},
"responseIsBinary": false
}
]
"""
Then I poll and finalize the ACME order order as finalized_order
And the value finalized_order.body with jq ".status" should be equal to "valid"
And I parse the full-chain certificate from order finalized_order as cert
# Note: somehow Pebble is issuing a cert without common name but just SANs
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
"""
[
"localhost"
]
"""
+102 -2
View File
@@ -2,6 +2,106 @@ Feature: Nonce
Scenario: Generate a new nonce Scenario: Generate a new nonce
Given I have an ACME cert profile as "acme_profile" Given I have an ACME cert profile as "acme_profile"
When I send a HEAD request to "/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce" When I send a "HEAD" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce"
Then the response status code should be "200" Then the response status code should be "200"
Then the response header "Replay-Nonce" should contains non-empty value And the response header "Replay-Nonce" should contains non-empty value
Scenario Outline: Send a bad nonce to account endpoints
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"
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
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
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 I memorize <src_var> with jq "<jq>" as <dest_var>
When I send a raw ACME request to "<url>"
"""
{
"protected": {
"alg": "RS256",
"nonce": "oFvnlFP1wIhRlYS2jTaXbA",
"url": "<url>",
"kid": "{acme_account.uri}"
},
"payload": {}
}
"""
Then the value response.status_code should be equal to 400
And the value response with jq ".status" should be equal to 400
And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce"
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} |
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"
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
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
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 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"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce_value}",
"url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders",
"kid": "{acme_account.uri}"
},
"payload": {}
}
"""
Then the value response.status_code should be equal to 200
And I memorize <src_var> with jq "<jq>" as <dest_var>
When I send a raw ACME request to "<url>"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce_value}",
"url": "<url>",
"kid": "{acme_account.uri}"
},
"payload": {}
}
"""
Then the value response.status_code should be equal to 400
And the value response with jq ".status" should be equal to 400
And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce"
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} |
+93 -25
View File
@@ -2,8 +2,7 @@ Feature: Order
Scenario: Create a new order Scenario: Create a new order
Given I have an ACME cert profile as "acme_profile" Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
# # TODO: make it I have an account already instead?
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -13,18 +12,17 @@ Feature: Order
} }
""" """
Then I create a RSA private key pair as cert_key Then I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then 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/pki/acme/profiles/{acme_profile.id}/orders/(.+)
Then the value order.body with jq ".status" should be equal to "pending" And the value order.body with jq ".status" should be equal to "pending"
Then the value order.body with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] And the value order.body with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}]
Then the value order.body with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize And the value order.body with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize
Then the value order.body with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true And the value order.body with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true
Scenario: Create a new order with SANs Scenario: Create a new order with SANs
Given I have an ACME cert profile as "acme_profile" Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
# # TODO: make it I have an account already instead?
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -33,17 +31,17 @@ Feature: Order
"COMMON_NAME": "localhost" "COMMON_NAME": "localhost"
} }
""" """
Then I add subject alternative name to certificate signing request csr And I add subject alternative name to certificate signing request csr
""" """
[ [
"example.com", "example.com",
"infisical.com" "infisical.com"
] ]
""" """
Then I create a RSA private key pair as cert_key And I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then the value order.body with jq ".identifiers | sort_by(.value)" should be equal to json And the value order.body with jq ".identifiers | sort_by(.value)" should be equal to json
""" """
[ [
{"type": "dns", "value": "example.com"}, {"type": "dns", "value": "example.com"},
@@ -54,8 +52,7 @@ Feature: Order
Scenario: Fetch an order Scenario: Fetch an order
Given I have an ACME cert profile as "acme_profile" Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
# # TODO: make it I have an account already instead?
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
@@ -65,10 +62,81 @@ Feature: Order
} }
""" """
Then I create a RSA private key pair as cert_key Then I create a RSA private key pair as cert_key
Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
Then I send an ACME post-as-get to order.uri as fetched_order And I send an ACME post-as-get to order.uri as fetched_order
Then the value fetched_order with jq ".status" should be equal to "pending" And the value fetched_order with jq ".status" should be equal to "pending"
Then the value fetched_order with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] And the value fetched_order with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}]
Then the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize And the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize
Then the value fetched_order with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true And the value fetched_order with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/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"
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"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order",
"kid": "{acme_account.uri}"
},
"payload": {
"identifiers": [
{ "type": "<identifier_type>", "value": "www.example.org" }
]
}
}
"""
Then the value response.status_code should be equal to 400
And the value response with jq ".status" should be equal to 400
And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier"
And the value response with jq ".detail" should be equal to "Only DNS identifiers are supported"
Examples: Bad Identifier Types
| identifier_type |
| bad |
| ip |
| email |
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"
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"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order",
"kid": "{acme_account.uri}"
},
"payload": {
"identifiers": [
{ "type": "dns", "value": "<identifier_value>" }
]
}
}
"""
Then the value response.status_code should be equal to 400
And the value response with jq ".status" should be equal to 400
And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier"
And the value response with jq ".detail" should be equal to "Invalid DNS identifier"
Examples: Bad Identifier Vluaes
| identifier_value |
| 127.0.0.1 |
| 192.168.123.111 |
| 169.254.169.254 |
| ../../etc/passwd |
| !@#$ |
| ! |
| https://evil.com |
+518 -148
View File
@@ -1,29 +1,33 @@
import json import json
import logging import logging
import os
import re import re
import threading import urllib.parse
import httpx import acme.client
import jq import jq
import requests
import glom
from faker import Faker from faker import Faker
from acme import client from acme import client
from acme import messages from acme import messages
from acme import standalone from acme import standalone
from acme.jws import Signature
from behave.runner import Context from behave.runner import Context
from behave import given from behave import given
from behave import when from behave import when
from behave import then from behave import then
from josepy.jwk import JWKRSA from josepy.jwk import JWKRSA
from josepy import JSONObjectWithFields from josepy import json_util
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography import x509 from cryptography import x509
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from features.steps.utils import define_nock, clean_all_nock, restore_nock
from utils import replace_vars, with_nocks
from utils import eval_var
from utils import prepare_headers
ACC_KEY_BITS = 2048 ACC_KEY_BITS = 2048
ACC_KEY_PUBLIC_EXPONENT = 65537 ACC_KEY_PUBLIC_EXPONENT = 65537
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -37,96 +41,6 @@ class AcmeProfile:
self.eab_secret = eab_secret self.eab_secret = eab_secret
def replace_vars(payload: dict | list | int | float | str, vars: dict):
if isinstance(payload, dict):
return {
replace_vars(key, vars): replace_vars(value, vars)
for key, value in payload.items()
}
elif isinstance(payload, list):
return [replace_vars(item, vars) for item in payload]
elif isinstance(payload, str):
return payload.format(**vars)
else:
return payload
def parse_glom_path(path_str: str) -> glom.Path:
"""
Parse a glom path string with 'attr[index]' syntax into a Path object.
Examples:
>>> parse_glom_path('authorizations[0]') == Path('authorizations', 0)
True
>>> parse_glom_path('data.items[1].name') == Path('data', 'items', 1, 'name')
True
>>> parse_glom_path('user.addresses[0].street') == Path('user', 'addresses', 0, 'street')
True
"""
parts = []
# Split by dots, but preserve bracketed content
tokens = re.split(r"(?<!\[)\.(?![^\[]*\])", path_str)
for token in tokens:
token = token.strip()
if not token:
continue
# Check for attr[index] pattern
match = re.match(r"^(.+?)\[([^\]]+)\]$", token)
if match:
attr_name = match.group(1).strip()
index_str = match.group(2).strip()
# Parse index (support integers, slices, etc.)
if index_str.isdigit():
index = int(index_str)
elif "-" in index_str:
# Handle negative indices like [-1]
index = int(index_str)
elif ":" in index_str:
# Handle slices like [0:10]
index = slice(
*map(int, [x.strip() for x in index_str.split(":") if x.strip()])
)
else:
# Treat as string key
index = index_str
parts.extend([attr_name, index])
else:
# Plain attribute/key
parts.append(token)
return glom.Path(*parts)
def eval_var(context: Context, var_path: str, as_json: bool = True):
parts = var_path.split(".", 1)
value = context.vars[parts[0]]
if len(parts) == 2:
value = glom.glom(value, parse_glom_path(parts[1]))
if as_json:
if isinstance(value, JSONObjectWithFields):
value = value.to_json()
elif isinstance(value, requests.Response):
value = value.json()
elif isinstance(value, httpx.Response):
value = value.json()
return value
def prepare_headers(context: Context) -> dict | None:
headers = {}
auth_token = getattr(context, "auth_token", None)
if auth_token is not None:
headers["authorization"] = "Bearer {}".format(auth_token)
if not headers:
return None
return headers
@given("I make a random {faker_type} as {var_name}") @given("I make a random {faker_type} as {var_name}")
def step_impl(context: Context, faker_type: str, var_name: str): def step_impl(context: Context, faker_type: str, var_name: str):
context.vars[var_name] = getattr(faker, faker_type)() context.vars[var_name] = getattr(faker, faker_type)()
@@ -134,12 +48,39 @@ def step_impl(context: Context, faker_type: str, var_name: str):
@given('I have an ACME cert profile as "{profile_var}"') @given('I have an ACME cert profile as "{profile_var}"')
def step_impl(context: Context, profile_var: str): def step_impl(context: Context, profile_var: str):
# TODO: Fixed value for now, just to make test much easier, profile_id = context.vars.get("PROFILE_ID")
# we should call infisical API to create such profile instead secret = context.vars.get("EAB_SECRET")
# in the future if profile_id is not None and secret is not None:
profile_id = os.getenv("PROFILE_ID") kid = profile_id
kid = profile_id else:
secret = os.getenv("EAB_SECRET") profile_slug = faker.slug()
jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post(
"/api/v1/pki/certificate-profiles",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"projectId": context.vars["PROJECT_ID"],
"slug": profile_slug,
"description": "ACME Profile created by BDD test",
"enrollmentType": "acme",
"caId": context.vars["CERT_CA_ID"],
"certificateTemplateId": context.vars["CERT_TEMPLATE_ID"],
"acmeConfig": {},
},
)
response.raise_for_status()
resp_json = response.json()
profile_id = resp_json["certificateProfile"]["id"]
kid = profile_id
response = context.http_client.get(
f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
headers=dict(authorization="Bearer {}".format(jwt_token)),
)
response.raise_for_status()
resp_json = response.json()
secret = resp_json["eabSecret"]
context.vars[profile_var] = AcmeProfile( context.vars[profile_var] = AcmeProfile(
profile_id, profile_id,
eab_kid=kid, eab_kid=kid,
@@ -147,12 +88,204 @@ def step_impl(context: Context, profile_var: str):
) )
@given("I create a Cloudflare connection as {var_name}")
def step_impl(context: Context, var_name: str):
jwt_token = context.vars["AUTH_TOKEN"]
conn_slug = faker.slug()
mock_account_id = "MOCK_ACCOUNT_ID"
with with_nocks(
context,
definitions=[
{
"scope": "https://api.cloudflare.com:443",
"method": "GET",
"path": f"/client/v4/accounts/{mock_account_id}",
"status": 200,
"response": {
"result": {
"id": "A2A6347F-88B5-442D-9798-95E408BC7701",
"name": "Mock Account",
"type": "standard",
"settings": {
"enforce_twofactor": True,
"api_access_enabled": None,
"access_approval_expiry": None,
"abuse_contact_email": None,
"user_groups_ui_beta": False,
},
"legacy_flags": {
"enterprise_zone_quota": {
"maximum": 0,
"current": 0,
"available": 0,
}
},
"created_on": "2013-04-18T00:41:02.215243Z",
},
"success": True,
"errors": [],
"messages": [],
},
"responseIsBinary": False,
}
],
):
response = context.http_client.post(
"/api/v1/app-connections/cloudflare",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"name": conn_slug,
"description": "",
"method": "api-token",
"credentials": {
"apiToken": "MOCK_API_TOKEN",
"accountId": mock_account_id,
},
},
)
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",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"projectId": context.vars["PROJECT_ID"],
"name": ca_slug,
"type": "acme",
"status": "active",
"enableDirectIssuance": True,
"configuration": config,
},
)
response.raise_for_status()
context.vars[var_name] = response
@given("I create a certificate template with the following config as {var_name}")
def step_impl(context: Context, var_name: str):
jwt_token = context.vars["AUTH_TOKEN"]
template_slug = faker.slug()
config = replace_vars(json.loads(context.text), context.vars)
response = context.http_client.post(
"/api/v2/certificate-templates",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"projectId": context.vars["PROJECT_ID"],
"name": template_slug,
"description": "",
}
| config,
)
response.raise_for_status()
context.vars[var_name] = response
@given(
'I create an ACME profile with ca {ca_id} and template {template_id} as "{profile_var}"'
)
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",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"projectId": context.vars["PROJECT_ID"],
"slug": profile_slug,
"description": "ACME Profile created by BDD test",
"enrollmentType": "acme",
"caId": replace_vars(ca_id, context.vars),
"certificateTemplateId": replace_vars(template_id, context.vars),
"acmeConfig": {},
},
)
response.raise_for_status()
resp_json = response.json()
profile_id = resp_json["certificateProfile"]["id"]
kid = profile_id
response = context.http_client.get(
f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
headers=dict(authorization="Bearer {}".format(jwt_token)),
)
response.raise_for_status()
resp_json = response.json()
secret = resp_json["eabSecret"]
context.vars[profile_var] = AcmeProfile(
profile_id,
eab_kid=kid,
eab_secret=secret,
)
@given('I have an ACME cert profile with external ACME CA as "{profile_var}"')
def step_impl(context: Context, profile_var: str):
profile_id = context.vars.get("PROFILE_ID")
secret = context.vars.get("EAB_SECRET")
if profile_id is not None and secret is not None:
kid = profile_id
else:
profile_slug = faker.slug()
jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post(
"/api/v1/pki/certificate-profiles",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"projectId": context.vars["PROJECT_ID"],
"slug": profile_slug,
"description": "ACME Profile created by BDD test",
"enrollmentType": "acme",
"caId": context.vars["CERT_CA_ID"],
"certificateTemplateId": context.vars["CERT_TEMPLATE_ID"],
"acmeConfig": {},
},
)
response.raise_for_status()
resp_json = response.json()
profile_id = resp_json["certificateProfile"]["id"]
kid = profile_id
response = context.http_client.get(
f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
headers=dict(authorization="Bearer {}".format(jwt_token)),
)
response.raise_for_status()
resp_json = response.json()
secret = resp_json["eabSecret"]
context.vars[profile_var] = AcmeProfile(
profile_id,
eab_kid=kid,
eab_secret=secret,
)
@given("I intercept outgoing requests")
def step_impl(context: Context):
definitions = replace_vars(json.loads(context.text), context.vars)
define_nock(context, definitions)
@then("I reset requests interceptions")
def step_impl(context: Context):
clean_all_nock(context)
restore_nock(context)
@given("I use {token_var} for authentication") @given("I use {token_var} for authentication")
def step_impl(context: Context, token_var: str): def step_impl(context: Context, token_var: str):
context.auth_token = eval_var(context, token_var) context.auth_token = eval_var(context, token_var)
@when('I send a {method} request to "{url}"') @when('I send a "{method}" request to "{url}"')
def step_impl(context: Context, method: str, url: str): def step_impl(context: Context, method: str, url: str):
logger.debug("Sending %s request to %s", method, url) logger.debug("Sending %s request to %s", method, url)
response = context.http_client.request( response = context.http_client.request(
@@ -166,7 +299,7 @@ def step_impl(context: Context, method: str, url: str):
pass pass
@when('I send a {method} request to "{url}" with JSON payload') @when('I send a "{method}" request to "{url}" with JSON payload')
def step_impl(context: Context, method: str, url: str): def step_impl(context: Context, method: str, url: str):
json_payload = json.loads(context.text) json_payload = json.loads(context.text)
json_payload = replace_vars(json_payload, context.vars) json_payload = replace_vars(json_payload, context.vars)
@@ -187,23 +320,34 @@ def step_impl(context: Context, method: str, url: str):
logger.debug("Response JSON payload: %r", response.json()) logger.debug("Response JSON payload: %r", response.json())
@when("I have an ACME client connecting to {url}") def create_acme_client(context: Context, url: str, acc_jwk: JWKRSA | None = None):
def step_impl(context: Context, url: str): if acc_jwk is None:
private_key = rsa.generate_private_key( private_key = rsa.generate_private_key(
public_exponent=ACC_KEY_PUBLIC_EXPONENT, key_size=ACC_KEY_BITS public_exponent=ACC_KEY_PUBLIC_EXPONENT, key_size=ACC_KEY_BITS
) )
pem_bytes = private_key.private_bytes( pem_bytes = private_key.private_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8, format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(), encryption_algorithm=serialization.NoEncryption(),
) )
acc_jwk = JWKRSA.load(pem_bytes) acc_jwk = JWKRSA.load(pem_bytes)
net = client.ClientNetwork(acc_jwk) net = client.ClientNetwork(acc_jwk)
directory_url = url.format(**context.vars) directory_url = url.format(**context.vars)
directory = client.ClientV2.get_directory(directory_url, net) directory = client.ClientV2.get_directory(directory_url, net)
context.acme_client = client.ClientV2(directory, net=net) context.acme_client = client.ClientV2(directory, net=net)
@when('I have an ACME client connecting to "{url}"')
def step_impl(context: Context, url: str):
create_acme_client(context, url)
@when('I have an ACME client connecting to "{url}" with the key pair from {client_var}')
def step_impl(context: Context, url: str, client_var: str):
another_client = eval_var(context, client_var, as_json=False)
create_acme_client(context, url, acc_jwk=another_client.net.key)
@then('the response status code should be "{expected_status_code:d}"') @then('the response status code should be "{expected_status_code:d}"')
def step_impl(context: Context, expected_status_code: int): def step_impl(context: Context, expected_status_code: int):
assert context.vars["response"].status_code == expected_status_code, ( assert context.vars["response"].status_code == expected_status_code, (
@@ -228,24 +372,131 @@ def step_impl(context: Context):
assert payload == replaced, f"{payload} != {replaced}" assert payload == replaced, f"{payload} != {replaced}"
@then( @when('I use a different new-account URL "{url}" for EAB signature')
'I register a new ACME account with email {email} and EAB key id "{kid}" with secret "{secret}" as {account_var}' def step_impl(context: Context, url: str):
) context.alt_eab_url = replace_vars(url, context.vars)
def step_impl(context: Context, email: str, kid: str, secret: str, account_var: str):
def register_account_with_eab(
context: Context,
email: str,
kid: str,
secret: str,
account_var: str,
only_return_existing: bool = False,
):
acme_client = context.acme_client acme_client = context.acme_client
account_public_key = acme_client.net.key.public_key() account_public_key = acme_client.net.key.public_key()
if hasattr(context, "alt_eab_url"):
eab_directory = messages.Directory.from_json(
{"newAccount": context.alt_eab_url}
)
else:
eab_directory = acme_client.directory
eab = messages.ExternalAccountBinding.from_data( eab = messages.ExternalAccountBinding.from_data(
account_public_key=account_public_key, account_public_key=account_public_key,
kid=replace_vars(kid, context.vars), kid=replace_vars(kid, context.vars),
hmac_key=replace_vars(secret, context.vars), hmac_key=replace_vars(secret, context.vars),
directory=acme_client.directory, directory=eab_directory,
hmac_alg="HS256", hmac_alg="HS256",
) )
registration = messages.NewRegistration.from_data( registration = messages.NewRegistration.from_data(
email=email, email=email,
external_account_binding=eab, external_account_binding=eab,
only_return_existing=only_return_existing,
) )
context.vars[account_var] = acme_client.new_account(registration) try:
context.vars[account_var] = acme_client.new_account(registration)
except Exception as exp:
context.vars["error"] = exp
@then(
'I register a new ACME account with email {email} and EAB key id "{kid}" with secret "{secret}" as {account_var}'
)
def step_impl(context: Context, email: str, kid: str, secret: str, account_var: str):
register_account_with_eab(
context=context, email=email, kid=kid, secret=secret, account_var=account_var
)
@then(
'I find the existing ACME account with email {email} and EAB key id "{kid}" with secret "{secret}" as {account_var}'
)
def step_impl(context: Context, email: str, kid: str, secret: str, account_var: str):
register_account_with_eab(
context=context,
email=email,
kid=kid,
secret=secret,
account_var=account_var,
only_return_existing=True,
)
@then("I register a new ACME account with email {email} without EAB")
def step_impl(context: Context, email: str):
acme_client = context.acme_client
registration = messages.NewRegistration.from_data(
email=email,
)
try:
context.vars["error"] = acme_client.new_account(registration)
except Exception as exp:
context.vars["error"] = exp
def send_raw_acme_req(context: Context, url: str):
acme_client = context.acme_client
content = json.loads(context.text)
protected = replace_vars(content["protected"], context.vars)
alg = acme_client.net.alg
if "raw_payload" in content:
encoded_payload = content["raw_payload"].encode("utf-8")
elif "payload" in content:
payload = (
replace_vars(content["payload"], context.vars)
if "payload" in content
else None
)
encoded_payload = json.dumps(payload).encode() if payload is not None else b""
else:
encoded_payload = b""
protected_headers = json.dumps(protected)
signature = alg.sign(
key=acme_client.net.key.key,
msg=Signature._msg(protected_headers, encoded_payload),
)
jws = json.dumps(
{
"protected": json_util.encode_b64jose(protected_headers.encode()),
"payload": json_util.encode_b64jose(encoded_payload),
"signature": json_util.encode_b64jose(signature),
}
)
base_url = context.vars["BASE_URL"]
actual_url = urllib.parse.urljoin(base_url, replace_vars(url, context.vars))
response = acme_client.net._send_request(
"POST",
actual_url,
data=jws,
headers={"Content-Type": acme.client.ClientNetwork.JOSE_CONTENT_TYPE},
)
context.vars["response"] = response
@when('I send a raw ACME request to "{url}"')
def step_impl(context: Context, url: str):
send_raw_acme_req(context, url)
@then(
"I encode CSR {pem_var} as JOSE Base-64 DER as {var_name}",
)
def step_impl(context: Context, pem_var: str, var_name: str):
csr = eval_var(context, pem_var)
parsed_csr = x509.load_pem_x509_csr(csr)
context.vars[var_name] = json_util.encode_csr(parsed_csr)
@then( @then(
@@ -384,10 +635,17 @@ def step_impl(context: Context, var_path: str):
@then("the value {var_path} should be equal to {expected}") @then("the value {var_path} should be equal to {expected}")
def step_impl(context: Context, var_path: str, expected: str): def step_impl(context: Context, var_path: str, expected: str):
value = eval_var(context, var_path) value = eval_var(context, var_path)
expected_value = json.loads(expected) expected_value = replace_vars(json.loads(expected), context.vars)
assert value == expected_value, f"{value!r} does not match {expected_value!r}" assert value == expected_value, f"{value!r} does not match {expected_value!r}"
@then("the value {var_path} should not be equal to {expected}")
def step_impl(context: Context, var_path: str, expected: str):
value = eval_var(context, var_path)
expected_value = replace_vars(json.loads(expected), context.vars)
assert value != expected_value, f"{value!r} does match {expected_value!r}"
@then('I memorize {var_path} with jq "{jq_query}" as {var_name}') @then('I memorize {var_path} with jq "{jq_query}" as {var_name}')
def step_impl(context: Context, var_path: str, jq_query, var_name: str): def step_impl(context: Context, var_path: str, jq_query, var_name: str):
_, value = apply_value_with_jq( _, value = apply_value_with_jq(
@@ -398,6 +656,19 @@ def step_impl(context: Context, var_path: str, jq_query, var_name: str):
context.vars[var_name] = value context.vars[var_name] = value
@then("I peak and memorize the next nonce as {var_name}")
def step_impl(context: Context, var_name: str):
acme_client = context.acme_client
context.vars[var_name] = json_util.encode_b64jose(list(acme_client.net._nonces)[0])
@then("I put away current ACME client as {var_name}")
def step_impl(context: Context, var_name: str):
acme_client = context.acme_client
del context.acme_client
context.vars[var_name] = acme_client
@then("I memorize {var_path} as {var_name}") @then("I memorize {var_path} as {var_name}")
def step_impl(context: Context, var_path: str, var_name: str): def step_impl(context: Context, var_path: str, var_name: str):
value = eval_var(context, var_path) value = eval_var(context, var_path)
@@ -410,51 +681,61 @@ def step_impl(context: Context, var_path: str):
print(json.dumps(value.json(), indent=2)) print(json.dumps(value.json(), indent=2))
@then( def select_challenge(
"I select challenge with type {challenge_type} for domain {domain} from order at {var_path} as {challenge_var}"
)
def step_impl(
context: Context, context: Context,
challenge_type: str, challenge_type: str,
order_var_path: str,
domain: str, domain: str,
var_path: str,
challenge_var: str,
): ):
order = eval_var(context, var_path, as_json=False) acme_client = context.acme_client
order = eval_var(context, order_var_path, as_json=False)
if isinstance(order, dict):
order_body = messages.Order.from_json(order)
order = messages.OrderResource(
body=order_body,
authorizations=[
acme_client._authzr_from_response(
acme_client._post_as_get(url), uri=url
)
for url in order_body.authorizations
],
)
if not isinstance(order, messages.OrderResource): if not isinstance(order, messages.OrderResource):
raise ValueError( raise ValueError(
f"Expected OrderResource but got {type(order)!r} at {var_path!r}" f"Expected OrderResource but got {type(order)!r} at {order_var_path!r}"
) )
auths = list( auths = list(
filter(lambda o: o.body.identifier.value == domain, order.authorizations) filter(lambda o: o.body.identifier.value == domain, order.authorizations)
) )
if not auths: if not auths:
raise ValueError( raise ValueError(
f"Authorization for domain {domain!r} not found in {var_path!r}" f"Authorization for domain {domain!r} not found in {order_var_path!r}"
) )
if len(auths) > 1: if len(auths) > 1:
raise ValueError( raise ValueError(
f"More than one order for domain {domain!r} found in {var_path!r}" f"More than one order for domain {domain!r} found in {order_var_path!r}"
) )
auth = auths[0] auth = auths[0]
challenges = list(filter(lambda a: a.typ == challenge_type, auth.body.challenges)) challenges = list(filter(lambda a: a.typ == challenge_type, auth.body.challenges))
if not challenges: if not challenges:
raise ValueError( raise ValueError(
f"Authorization type {challenge_type!r} not found in {var_path!r}" f"Authorization type {challenge_type!r} not found in {order_var_path!r}"
) )
if len(challenges) > 1: if len(challenges) > 1:
raise ValueError( raise ValueError(
f"More than one authorization for type {challenge_type!r} found in {var_path!r}" f"More than one authorization for type {challenge_type!r} found in {order_var_path!r}"
) )
context.vars[challenge_var] = challenges[0] return challenges[0]
@then("I serve challenge response for {var_path} at {hostname}") def serve_challenge(
def step_impl(context: Context, var_path: str, hostname: str): context: Context,
if hostname != "localhost": challenge: messages.ChallengeBody,
raise ValueError("Currently only localhost is supported") ):
challenge = eval_var(context, var_path, as_json=False) if hasattr(context, "web_server"):
context.web_server.shutdown_and_server_close()
response, validation = challenge.response_and_validation( response, validation = challenge.response_and_validation(
context.acme_client.net.key context.acme_client.net.key
) )
@@ -463,19 +744,101 @@ def step_impl(context: Context, var_path: str, hostname: str):
) )
# TODO: make port configurable # TODO: make port configurable
servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), {resource}) servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), {resource})
# Start client standalone web server. servers.serve_forever()
web_server = threading.Thread(name="web_server", target=servers.serve_forever) context.web_server = servers
web_server.daemon = True
web_server.start()
context.web_server = web_server def notify_challenge_ready(context: Context, challenge: messages.ChallengeBody):
acme_client = context.acme_client
response, validation = challenge.response_and_validation(acme_client.net.key)
acme_client.answer_challenge(challenge, response)
@then(
"I select challenge with type {challenge_type} for domain {domain} from order in {var_path} as {challenge_var}"
)
def step_impl(
context: Context,
challenge_type: str,
domain: str,
var_path: str,
challenge_var: str,
):
challenge = select_challenge(
context=context,
challenge_type=challenge_type,
domain=domain,
order_var_path=var_path,
)
context.vars[challenge_var] = challenge
@then("I pass all challenges with type {challenge_type} for order in {order_var_path}")
def step_impl(
context: Context,
challenge_type: str,
order_var_path: str,
):
acme_client = context.acme_client
order = eval_var(context, order_var_path, as_json=False)
if isinstance(order, dict):
order_body = messages.Order.from_json(order)
order = messages.OrderResource(
body=order_body,
authorizations=[
acme_client._authzr_from_response(
acme_client._post_as_get(url), uri=url
)
for url in order_body.authorizations
],
)
if not isinstance(order, messages.OrderResource):
raise ValueError(
f"Expected OrderResource but got {type(order)!r} at {order_var_path!r}"
)
for domain in order.body.identifiers:
logger.info(
"Selecting challenge for domain %s with type %s ...",
domain.value,
challenge_type,
)
challenge = select_challenge(
context=context,
challenge_type=challenge_type,
domain=domain.value,
order_var_path=order_var_path,
)
logger.info(
"Found challenge for domain %s with type %s, challenge=%s",
domain.value,
challenge_type,
challenge.uri,
)
logger.info(
"Serving challenge for domain %s with type %s ...",
domain.value,
challenge_type,
)
serve_challenge(context=context, challenge=challenge)
logger.info(
"Notifying challenge for domain %s with type %s ...", domain, challenge_type
)
notify_challenge_ready(context=context, challenge=challenge)
@then("I serve challenge response for {var_path} at {hostname}")
def step_impl(context: Context, var_path: str, hostname: str):
challenge = eval_var(context, var_path, as_json=False)
serve_challenge(context=context, challenge=challenge)
@then("I tell ACME server that {var_path} is ready to be verified") @then("I tell ACME server that {var_path} is ready to be verified")
def step_impl(context: Context, var_path: str): def step_impl(context: Context, var_path: str):
challenge = eval_var(context, var_path, as_json=False) challenge = eval_var(context, var_path, as_json=False)
acme_client = context.acme_client notify_challenge_ready(context=context, challenge=challenge)
response, validation = challenge.response_and_validation(acme_client.net.key)
acme_client.answer_challenge(challenge, response)
@then("I poll and finalize the ACME order {var_path} as {finalized_var}") @then("I poll and finalize the ACME order {var_path} as {finalized_var}")
@@ -484,3 +847,10 @@ def step_impl(context: Context, var_path: str, finalized_var: str):
acme_client = context.acme_client acme_client = context.acme_client
finalized_order = acme_client.poll_and_finalize(order) finalized_order = acme_client.poll_and_finalize(order)
context.vars[finalized_var] = finalized_order context.vars[finalized_var] = finalized_order
@then("I parse the full-chain certificate from order {order_var_path} as {cert_var}")
def step_impl(context: Context, order_var_path: str, cert_var: str):
order = eval_var(context, order_var_path, as_json=False)
cert = x509.load_pem_x509_certificate(order.fullchain_pem.encode())
context.vars[cert_var] = cert
+302
View File
@@ -0,0 +1,302 @@
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.x509.oid import NameOID
import logging
import re
import contextlib
import httpx
import requests
import requests.structures
import glom
from faker import Faker
from behave.runner import Context
from josepy import JSONObjectWithFields
ACC_KEY_BITS = 2048
ACC_KEY_PUBLIC_EXPONENT = 65537
logger = logging.getLogger(__name__)
faker = Faker()
class AcmeProfile:
def __init__(self, id: str, eab_kid: str, eab_secret: str):
self.id = id
self.eab_kid = eab_kid
self.eab_secret = eab_secret
def replace_vars(payload: dict | list | int | float | str, vars: dict):
if isinstance(payload, dict):
return {
replace_vars(key, vars): replace_vars(value, vars)
for key, value in payload.items()
}
elif isinstance(payload, list):
return [replace_vars(item, vars) for item in payload]
elif isinstance(payload, str):
return payload.format(**vars)
else:
return payload
def parse_glom_path(path_str: str) -> glom.Path:
"""
Parse a glom path string with 'attr[index]' syntax into a Path object.
Examples:
>>> parse_glom_path('authorizations[0]') == Path('authorizations', 0)
True
>>> parse_glom_path('data.items[1].name') == Path('data', 'items', 1, 'name')
True
>>> parse_glom_path('user.addresses[0].street') == Path('user', 'addresses', 0, 'street')
True
"""
parts = []
# Split by dots, but preserve bracketed content
tokens = re.split(r"(?<!\[)\.(?![^\[]*\])", path_str)
for token in tokens:
token = token.strip()
if not token:
continue
# Check for attr[index] pattern
match = re.match(r"^(.+?)\[([^\]]+)\]$", token)
if match:
attr_name = match.group(1).strip()
index_str = match.group(2).strip()
# Parse index (support integers, slices, etc.)
if index_str.isdigit():
index = int(index_str)
elif "-" in index_str:
# Handle negative indices like [-1]
index = int(index_str)
elif ":" in index_str:
# Handle slices like [0:10]
index = slice(
*map(int, [x.strip() for x in index_str.split(":") if x.strip()])
)
else:
# Treat as string key
index = index_str
parts.extend([attr_name, index])
else:
# Plain attribute/key
parts.append(token)
return glom.Path(*parts)
def eval_var(context: Context, var_path: str, as_json: bool = True):
parts = var_path.split(".", 1)
value = context.vars[parts[0]]
if len(parts) == 2:
value = glom.glom(value, parse_glom_path(parts[1]))
if as_json:
if isinstance(value, JSONObjectWithFields):
value = value.to_json()
elif isinstance(value, requests.Response):
value = value.json()
elif isinstance(value, requests.structures.CaseInsensitiveDict):
value = dict(value.lower_items())
elif isinstance(value, httpx.Response):
value = value.json()
elif isinstance(value, x509.Certificate):
value = x509_cert_to_dict(value)
return value
def prepare_headers(context: Context) -> dict | None:
headers = {}
auth_token = getattr(context, "auth_token", None)
if auth_token is not None:
headers["authorization"] = "Bearer {}".format(auth_token)
if not headers:
return None
return headers
def x509_cert_to_dict(cert: x509.Certificate) -> dict:
"""
Convert a cryptography.x509.Certificate to a JSON-serializable nested dict
with human-readable keys.
"""
def oid_to_name(oid):
# Map known OIDs to human-readable names
mapping = {
NameOID.COMMON_NAME: "common_name",
NameOID.ORGANIZATION_NAME: "organization",
NameOID.ORGANIZATIONAL_UNIT_NAME: "organizational_unit",
NameOID.COUNTRY_NAME: "country",
NameOID.LOCALITY_NAME: "locality",
NameOID.STATE_OR_PROVINCE_NAME: "state_or_province",
NameOID.EMAIL_ADDRESS: "email_address",
NameOID.SERIAL_NUMBER: "serial_number",
NameOID.SURNAME: "surname",
NameOID.GIVEN_NAME: "given_name",
NameOID.TITLE: "title",
NameOID.JURISDICTION_COUNTRY_NAME: "jurisdiction_country",
NameOID.JURISDICTION_STATE_OR_PROVINCE_NAME: "jurisdiction_state",
NameOID.JURISDICTION_LOCALITY_NAME: "jurisdiction_locality",
NameOID.BUSINESS_CATEGORY: "business_category",
NameOID.POSTAL_CODE: "postal_code",
NameOID.STREET_ADDRESS: "street_address",
NameOID.DOMAIN_COMPONENT: "domain_component",
NameOID.USER_ID: "user_id",
# Add more as needed
}
return mapping.get(oid, oid.dotted_string)
def name_to_dict(name: x509.Name) -> dict:
return {oid_to_name(attr.oid): attr.value for attr in name}
def dns_to_dict(dns: x509.DNSName) -> dict:
return dict(value=dns.value)
def extension_to_dict(ext):
if isinstance(ext.value, x509.SubjectAlternativeName):
return {
"critical": ext.critical,
"general_names": [dns_to_dict(gn) for gn in ext.value],
}
elif isinstance(ext.value, x509.BasicConstraints):
return {
"critical": ext.critical,
"ca": ext.value.ca,
"path_length": ext.value.path_length,
}
elif isinstance(ext.value, x509.KeyUsage):
return {
"critical": ext.critical,
**{
field.lower(): getattr(ext.value, field)
for field in [
"digital_signature",
"content_commitment",
"key_encipherment",
"data_encipherment",
"key_agreement",
"key_cert_sign",
"crl_sign",
# TODO: deal with error: "ValueError: encipher_only is undefined unless key_agreement is true"
# "encipher_only",
# "decipher_only",
]
if getattr(ext.value, field) is not None
},
}
elif isinstance(ext.value, x509.ExtendedKeyUsage):
return {
"critical": ext.critical,
"usages": [eku.dotted_string for eku in ext.value],
}
elif isinstance(ext.value, x509.CRLDistributionPoints):
return {
"critical": ext.critical,
"distribution_points": [
{
"full_name": [str(uri) for uri in dp.full_name]
if dp.full_name
else None,
"crl_issuer": [str(issuer) for issuer in dp.crl_issuer]
if dp.crl_issuer
else None,
"reasons": [r.name for r in dp.reasons] if dp.reasons else None,
}
for dp in ext.value
],
}
elif isinstance(ext.value, x509.AuthorityKeyIdentifier):
return {
"critical": ext.critical,
"key_identifier": ext.value.key_identifier.hex()
if ext.value.key_identifier
else None,
"authority_cert_issuer": [
str(n) for n in ext.value.authority_cert_issuer
]
if ext.value.authority_cert_issuer
else None,
"authority_cert_serial_number": ext.value.authority_cert_serial_number,
}
elif isinstance(ext.value, x509.SubjectKeyIdentifier):
return {"critical": ext.critical, "digest": ext.value.digest.hex()}
else:
return {
"critical": ext.critical,
"oid": ext.oid.dotted_string,
"value": str(ext.value),
}
# Build the main dict
result = dict(
version=cert.version.name,
serial_number=cert.serial_number,
signature_algorithm=cert.signature_algorithm_oid._name,
issuer=name_to_dict(cert.issuer),
subject=name_to_dict(cert.subject),
validity={
"not_valid_before": cert.not_valid_before.isoformat(),
"not_valid_after": cert.not_valid_after.isoformat(),
},
public_key={
"key_size": cert.public_key().key_size,
},
extensions={
ext.oid._name
if hasattr(ext.oid, "_name") and ext.oid._name
else ext.oid.dotted_string: extension_to_dict(ext)
for ext in cert.extensions
},
fingerprint={
"sha1": cert.fingerprint(hashes.SHA1()).hex(),
"sha256": cert.fingerprint(hashes.SHA256()).hex(),
},
)
return result
def define_nock(context: Context, definitions: list[dict]):
jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post(
"/api/v1/bdd-nock/define",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json=dict(definitions=definitions),
)
response.raise_for_status()
def restore_nock(context: Context):
jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post(
"/api/v1/bdd-nock/restore",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json=dict(),
)
response.raise_for_status()
def clean_all_nock(context: Context):
jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post(
"/api/v1/bdd-nock/clean-all",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json=dict(),
)
response.raise_for_status()
@contextlib.contextmanager
def with_nocks(context: Context, definitions: list[dict]):
try:
define_nock(context, definitions)
yield
finally:
clean_all_nock(context)
restore_nock(context)
+13
View File
@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIICBDCCAYmgAwIBAgIIHZvNVJSPdsYwCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
bWluaWNhIHJvb3QgY2EgN2ZlMDQwMB4XDTI1MTExMzAwMzAxMloXDTI3MTIxMzAw
MzAxMlowFDESMBAGA1UEAxMJbG9jYWxob3N0MHYwEAYHKoZIzj0CAQYFK4EEACID
YgAE2V5oM5JimqDjzEfH10cKu6L8eQ9rxzkULbIJRFFuuXtKQQwkcAW8L4UuMkmG
lu5hFCBR8saHDpISuAyYLYqsddxwndxmGT3zyw6oU+8oXWX0tThL0KgajmZckOfR
ysYpo4GbMIGYMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBSIDfQe2L6+9aYyBFbd
t0S51xW3UDA4BgNVHREEMTAvgglsb2NhbGhvc3SCBnBlYmJsZYIUaG9zdC5kb2Nr
ZXIuaW50ZXJuYWyHBH8AAAEwCgYIKoZIzj0EAwMDaQAwZgIxAPkeGVzCDKuJYd/1
87+lXXtlMHrW7F+Rn1kyR8SBud2hDt5r3a+ZZ8IQ9aHazRia/AIxAOI4I41jwxf0
86i7fKx8of4s/CBc4+PF0hbCBkmen3aKuiZ7ueYuEsSNT6zHV2xc2w==
-----END CERTIFICATE-----
+6
View File
@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBx7d0VqxwTYcJajFgz
ja0PExBmxdZjEQRfGCMQY8GfHa0WpBUEwVtBD6XOGE5xZB2hZANiAATZXmgzkmKa
oOPMR8fXRwq7ovx5D2vHORQtsglEUW65e0pBDCRwBbwvhS4ySYaW7mEUIFHyxocO
khK4DJgtiqx13HCd3GYZPfPLDqhT7yhdZfS1OEvQqBqOZlyQ59HKxik=
-----END PRIVATE KEY-----
+28
View File
@@ -0,0 +1,28 @@
{
"pebble": {
"listenAddress": "0.0.0.0:14000",
"managementListenAddress": "0.0.0.0:15000",
"certificate": "/var/data/pebble/localhost/cert.pem",
"privateKey": "/var/data/pebble/localhost/key.pem",
"httpPort": 5002,
"tlsPort": 5001,
"ocspResponderURL": "",
"externalAccountBindingRequired": false,
"domainBlocklist": ["blocked-domain.example"],
"retryAfter": {
"authz": 3,
"order": 5
},
"keyAlgorithm": "ecdsa",
"profiles": {
"default": {
"description": "The profile you know and love",
"validityPeriod": 7776000
},
"shortlived": {
"description": "A short-lived cert profile, without actual enforcement",
"validityPeriod": 518400
}
}
}
}
+6
View File
@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDnPx90G0J4ba0CMTrh
AT0kJkRGyhv5ePWyobdT75za/I9MpU/VsC8BG5uJBraxiSOhZANiAAQWEiTINq0t
j+6Qiyzin74FU4/zLNuEs1FnipFn+Vb1W8qhvbBwLOGsANpaHIg4dpR+CghfccRQ
0kQm/AMgj08VXvta6vV7aQ8yk+/Cp6l4SVQ9GzizHiJ//Qb71vrXbco=
-----END PRIVATE KEY-----
+13
View File
@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB+zCCAYKgAwIBAgIIf+BA3XMRozcwCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
bWluaWNhIHJvb3QgY2EgN2ZlMDQwMCAXDTI1MTExMzAwMzAxMloYDzIxMjUxMTEz
MDAzMDEyWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA3ZmUwNDAwdjAQBgcq
hkjOPQIBBgUrgQQAIgNiAAQWEiTINq0tj+6Qiyzin74FU4/zLNuEs1FnipFn+Vb1
W8qhvbBwLOGsANpaHIg4dpR+CghfccRQ0kQm/AMgj08VXvta6vV7aQ8yk+/Cp6l4
SVQ9GzizHiJ//Qb71vrXbcqjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQW
MBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud
DgQWBBSIDfQe2L6+9aYyBFbdt0S51xW3UDAfBgNVHSMEGDAWgBSIDfQe2L6+9aYy
BFbdt0S51xW3UDAKBggqhkjOPQQDAwNnADBkAjAK2OUUVHs2LVqwyLEqIrXbc3gw
5r5p9TC9asqPN8vJxlTRStrXnJQRSQ2KoWztiSICMEV5jZGVk6TaUwlqcGmXEmGr
iFeQ3rXLaRw8XKMqj7+EiwaCD1o2wLgzny/21NFtxQ==
-----END CERTIFICATE-----
+97
View File
@@ -177,6 +177,7 @@
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"nock": "^14.0.10",
"nodemon": "^3.0.2", "nodemon": "^3.0.2",
"pino-pretty": "^10.2.3", "pino-pretty": "^10.2.3",
"prompt-sync": "^4.2.0", "prompt-sync": "^4.2.0",
@@ -9705,6 +9706,24 @@
"win32" "win32"
] ]
}, },
"node_modules/@mswjs/interceptors": {
"version": "0.39.8",
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz",
"integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@open-draft/deferred-promise": "^2.2.0",
"@open-draft/logger": "^0.3.0",
"@open-draft/until": "^2.0.0",
"is-node-process": "^1.2.0",
"outvariant": "^1.4.3",
"strict-event-emitter": "^0.5.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "15.5.2", "version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz",
@@ -10714,6 +10733,31 @@
"urijs": "^1.19.11" "urijs": "^1.19.11"
} }
}, },
"node_modules/@open-draft/deferred-promise": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
"integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
"dev": true,
"license": "MIT"
},
"node_modules/@open-draft/logger": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
"integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-node-process": "^1.2.0",
"outvariant": "^1.4.0"
}
},
"node_modules/@open-draft/until": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
"dev": true,
"license": "MIT"
},
"node_modules/@opentelemetry/api": { "node_modules/@opentelemetry/api": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -22958,6 +23002,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-node-process": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
"integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
"dev": true,
"license": "MIT"
},
"node_modules/is-number": { "node_modules/is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -23507,6 +23558,13 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true "dev": true
}, },
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true,
"license": "ISC"
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -25074,6 +25132,21 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/nock": {
"version": "14.0.10",
"resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz",
"integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mswjs/interceptors": "^0.39.5",
"json-stringify-safe": "^5.0.1",
"propagate": "^2.0.0"
},
"engines": {
"node": ">=18.20.0 <20 || >=20.12.1"
}
},
"node_modules/node-abi": { "node_modules/node-abi": {
"version": "3.65.0", "version": "3.65.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz",
@@ -27702,6 +27775,13 @@
"@otplib/preset-v11": "^12.0.1" "@otplib/preset-v11": "^12.0.1"
} }
}, },
"node_modules/outvariant": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
"integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
"dev": true,
"license": "MIT"
},
"node_modules/p-finally": { "node_modules/p-finally": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@@ -29103,6 +29183,16 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/propagate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/proto3-json-serializer": { "node_modules/proto3-json-serializer": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
@@ -31601,6 +31691,13 @@
"node": ">=4.0.0" "node": ">=4.0.0"
} }
}, },
"node_modules/strict-event-emitter": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
"integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
"dev": true,
"license": "MIT"
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+2
View File
@@ -44,6 +44,7 @@
"test:e2e": "vitest run -c vitest.e2e.config.mts --bail=1", "test:e2e": "vitest run -c vitest.e2e.config.mts --bail=1",
"test:e2e-watch": "vitest -c vitest.e2e.config.mts --bail=1", "test:e2e-watch": "vitest -c vitest.e2e.config.mts --bail=1",
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.mts", "test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.mts",
"test:bdd": "cd bdd && uv run behave",
"generate:component": "tsx ./scripts/create-backend-file.ts", "generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas", "generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas",
"auditlog-migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:latest", "auditlog-migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:latest",
@@ -122,6 +123,7 @@
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"nock": "^14.0.10",
"nodemon": "^3.0.2", "nodemon": "^3.0.2",
"pino-pretty": "^10.2.3", "pino-pretty": "^10.2.3",
"prompt-sync": "^4.2.0", "prompt-sync": "^4.2.0",
+3 -1
View File
@@ -91,6 +91,7 @@ import { TIdentityProjectServiceFactory } from "@app/services/identity-project/i
import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types"; import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types";
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TScopedIdentityV2ServiceFactory } from "@app/services/identity-v2/identity-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
import { TMembershipGroupServiceFactory } from "@app/services/membership-group/membership-group-service"; import { TMembershipGroupServiceFactory } from "@app/services/membership-group/membership-group-service";
@@ -258,7 +259,8 @@ declare module "fastify" {
integrationAuth: TIntegrationAuthServiceFactory; integrationAuth: TIntegrationAuthServiceFactory;
webhook: TWebhookServiceFactory; webhook: TWebhookServiceFactory;
serviceToken: TServiceTokenServiceFactory; serviceToken: TServiceTokenServiceFactory;
identity: TIdentityServiceFactory; identityV1: TIdentityServiceFactory;
identityV2: TScopedIdentityV2ServiceFactory;
identityAccessToken: TIdentityAccessTokenServiceFactory; identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory; identityProject: TIdentityProjectServiceFactory;
identityTokenAuth: TIdentityTokenAuthServiceFactory; identityTokenAuth: TIdentityTokenAuthServiceFactory;
@@ -0,0 +1,22 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasProjectIdCol = await knex.schema.hasColumn(TableName.Identity, "projectId");
if (!hasProjectIdCol) {
await knex.schema.alterTable(TableName.Identity, (t) => {
t.string("projectId");
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasProjectIdCol = await knex.schema.hasColumn(TableName.Identity, "projectId");
if (hasProjectIdCol) {
await knex.schema.alterTable(TableName.Identity, (t) => {
t.dropColumn("projectId");
});
}
}
+2 -1
View File
@@ -14,7 +14,8 @@ export const IdentitiesSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
hasDeleteProtection: z.boolean().default(false), hasDeleteProtection: z.boolean().default(false),
orgId: z.string().uuid() orgId: z.string().uuid(),
projectId: z.string().nullable().optional()
}); });
export type TIdentities = z.infer<typeof IdentitiesSchema>; export type TIdentities = z.infer<typeof IdentitiesSchema>;
+1 -5
View File
@@ -1,5 +1,4 @@
import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-template-router"; import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-template-router";
import { getConfig } from "@app/lib/config/env";
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router"; import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
@@ -109,10 +108,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register( await server.register(
async (pkiRouter) => { async (pkiRouter) => {
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" }); await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
// Notice: current this feature is still in development and is not yet ready for production. await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" });
if (getConfig().isAcmeFeatureEnabled === true) {
await pkiRouter.register(registerPkiAcmeRouter, { prefix: "/acme" });
}
}, },
{ prefix: "/pki" } { prefix: "/pki" }
); );
+3 -10
View File
@@ -435,14 +435,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim(), projectId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash), secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
message: z message: z.string().trim().max(255).optional(),
.string()
.trim()
.min(1)
.max(255)
.refine((message) => message.trim() !== "", {
message: "Commit message cannot be empty"
}),
changes: z.object({ changes: z.object({
secrets: z.object({ secrets: z.object({
create: z create: z
@@ -546,7 +539,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
projectId: req.body.projectId, projectId: req.body.projectId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
message: req.body.message, message: req.body.message || "",
changes: { changes: {
secrets: req.body.changes.secrets, secrets: req.body.changes.secrets,
folders: req.body.changes.folders folders: req.body.changes.folders
@@ -564,7 +557,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
projectId: req.body.projectId, projectId: req.body.projectId,
environment: req.body.environment, environment: req.body.environment,
secretPath: req.body.secretPath, secretPath: req.body.secretPath,
message: req.body.message message: req.body.message || ""
} }
} }
}); });
+3 -13
View File
@@ -2,7 +2,6 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { AcmeMalformedError } from "@app/ee/services/pki-acme/pki-acme-errors";
import { import {
AcmeOrderResourceSchema, AcmeOrderResourceSchema,
CreateAcmeAccountResponseSchema, CreateAcmeAccountResponseSchema,
@@ -257,12 +256,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({ const { profileId, accountId } = await validateExistingAccount({
req req
}); });
if (payload !== "") {
throw new AcmeMalformedError({ detail: "Payload should be empty" });
}
return sendAcmeResponse( return sendAcmeResponse(
res, res,
profileId, profileId,
@@ -369,12 +365,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({ const { profileId, accountId } = await validateExistingAccount({
req req
}); });
if (payload !== "") {
throw new AcmeMalformedError({ detail: "Payload should be empty" });
}
res.type("application/pem-certificate-chain"); res.type("application/pem-certificate-chain");
return sendAcmeResponse( return sendAcmeResponse(
res, res,
@@ -405,10 +398,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req, res) => { handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({ req }); const { profileId, accountId } = await validateExistingAccount({ req });
if (payload !== "") {
throw new AcmeMalformedError({ detail: "Payload should be empty" });
}
return sendAcmeResponse( return sendAcmeResponse(
res, res,
profileId, profileId,
+3 -3
View File
@@ -42,7 +42,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { organization } = await server.services.subOrganization.createSubOrg({ const { organization } = await server.services.subOrganization.createSubOrg({
name: req.body.name, name: req.body.name,
@@ -95,7 +95,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { organizations } = await server.services.subOrganization.listSubOrgs({ const { organizations } = await server.services.subOrganization.listSubOrgs({
permissionActor: req.permission, permissionActor: req.permission,
@@ -137,7 +137,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { organization } = await server.services.subOrganization.updateSubOrg({ const { organization } = await server.services.subOrganization.updateSubOrg({
subOrgId: req.params.subOrgId, subOrgId: req.params.subOrgId,
@@ -2,6 +2,7 @@ import z from "zod";
import { AppConnections } from "@app/lib/api-docs"; import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { import {
BaseAppConnectionSchema, BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema, GenericCreateAppConnectionFieldsSchema,
@@ -48,7 +49,7 @@ export const SanitizedChefConnectionSchema = z.discriminatedUnion("method", [
BaseChefConnectionSchema.extend({ BaseChefConnectionSchema.extend({
method: z.literal(ChefConnectionMethod.UserKey), method: z.literal(ChefConnectionMethod.UserKey),
credentials: ChefConnectionUserKeyCredentialsSchema.pick({ serverUrl: true, orgName: true, userName: true }) credentials: ChefConnectionUserKeyCredentialsSchema.pick({ serverUrl: true, orgName: true, userName: true })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Chef]} (User Key)` }))
]); ]);
export const ValidateChefConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateChefConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -70,8 +71,10 @@ export const UpdateChefConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Chef)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Chef));
export const ChefConnectionListItemSchema = z.object({ export const ChefConnectionListItemSchema = z
name: z.literal("Chef"), .object({
app: z.literal(AppConnection.Chef), name: z.literal("Chef"),
methods: z.nativeEnum(ChefConnectionMethod).array() app: z.literal(AppConnection.Chef),
}); methods: z.nativeEnum(ChefConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Chef] }));
@@ -2,6 +2,7 @@ import z from "zod";
import { AppConnections } from "@app/lib/api-docs"; import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { import {
BaseAppConnectionSchema, BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema, GenericCreateAppConnectionFieldsSchema,
@@ -34,7 +35,7 @@ export const SanitizedOCIConnectionSchema = z.discriminatedUnion("method", [
region: true, region: true,
fingerprint: true fingerprint: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.OCI]} (Access Key)` }))
]); ]);
export const ValidateOCIConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateOCIConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -58,8 +59,10 @@ export const UpdateOCIConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OCI)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OCI));
export const OCIConnectionListItemSchema = z.object({ export const OCIConnectionListItemSchema = z
name: z.literal("OCI"), .object({
app: z.literal(AppConnection.OCI), name: z.literal("OCI"),
methods: z.nativeEnum(OCIConnectionMethod).array() app: z.literal(AppConnection.OCI),
}); methods: z.nativeEnum(OCIConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.OCI] }));
@@ -2,6 +2,7 @@ import z from "zod";
import { AppConnections } from "@app/lib/api-docs"; import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { import {
BaseAppConnectionSchema, BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema, GenericCreateAppConnectionFieldsSchema,
@@ -32,7 +33,7 @@ export const SanitizedOracleDBConnectionSchema = z.discriminatedUnion("method",
sslRejectUnauthorized: true, sslRejectUnauthorized: true,
sslCertificate: true sslCertificate: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.OracleDB]} (Username and Password)` }))
]); ]);
export const ValidateOracleDBConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateOracleDBConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -64,9 +65,11 @@ export const UpdateOracleDBConnectionSchema = z
}) })
); );
export const OracleDBConnectionListItemSchema = z.object({ export const OracleDBConnectionListItemSchema = z
name: z.literal("OracleDB"), .object({
app: z.literal(AppConnection.OracleDB), name: z.literal("OracleDB"),
methods: z.nativeEnum(OracleDBConnectionMethod).array(), app: z.literal(AppConnection.OracleDB),
supportsPlatformManagement: z.literal(true) methods: z.nativeEnum(OracleDBConnectionMethod).array(),
}); supportsPlatformManagement: z.literal(true)
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.OracleDB] }));
@@ -159,9 +159,22 @@ export enum EventType {
DELETE_TRUSTED_IP = "delete-trusted-ip", DELETE_TRUSTED_IP = "delete-trusted-ip",
CREATE_SERVICE_TOKEN = "create-service-token", // v2 CREATE_SERVICE_TOKEN = "create-service-token", // v2
DELETE_SERVICE_TOKEN = "delete-service-token", // v2 DELETE_SERVICE_TOKEN = "delete-service-token", // v2
CREATE_SUB_ORGANIZATION = "create-sub-organization",
UPDATE_SUB_ORGANIZATION = "update-sub-organization",
CREATE_IDENTITY = "create-identity", CREATE_IDENTITY = "create-identity",
UPDATE_IDENTITY = "update-identity", UPDATE_IDENTITY = "update-identity",
DELETE_IDENTITY = "delete-identity", DELETE_IDENTITY = "delete-identity",
CREATE_IDENTITY_ORG_MEMBERSHIP = "create-identity-org-membership",
UPDATE_IDENTITY_ORG_MEMBERSHIP = "update-identity-org-membership",
DELETE_IDENTITY_ORG_MEMBERSHIP = "delete-identity-org-membership",
CREATE_IDENTITY_PROJECT_MEMBERSHIP = "create-identity-project-membership",
UPDATE_IDENTITY_PROJECT_MEMBERSHIP = "update-identity-project-membership",
DELETE_IDENTITY_PROJECT_MEMBERSHIP = "delete-identity-project-membership",
MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE = "machine-identity-auth-template-create", MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE = "machine-identity-auth-template-create",
MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE = "machine-identity-auth-template-update", MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE = "machine-identity-auth-template-update",
MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE = "machine-identity-auth-template-delete", MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE = "machine-identity-auth-template-delete",
@@ -174,9 +187,6 @@ export enum EventType {
UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth", GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth",
CREATE_SUB_ORGANIZATION = "create-sub-organization",
UPDATE_SUB_ORGANIZATION = "update-sub-organization",
ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth", ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth", GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth",
@@ -355,6 +365,8 @@ export enum EventType {
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup", LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project", ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso", ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
USER_LOGIN = "user-login",
SELECT_ORGANIZATION = "select-organization",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
@@ -560,6 +572,7 @@ interface UserActorMetadata {
email?: string | null; email?: string | null;
username: string; username: string;
permission?: Record<string, unknown>; permission?: Record<string, unknown>;
authMethod?: string;
} }
interface ServiceActorMetadata { interface ServiceActorMetadata {
@@ -891,6 +904,7 @@ interface CreateIdentityEvent {
identityId: string; identityId: string;
name: string; name: string;
hasDeleteProtection: boolean; hasDeleteProtection: boolean;
metadata?: { key: string; value: string }[];
}; };
} }
@@ -900,6 +914,7 @@ interface UpdateIdentityEvent {
identityId: string; identityId: string;
name?: string; name?: string;
hasDeleteProtection?: boolean; hasDeleteProtection?: boolean;
metadata?: { key: string; value: string }[];
}; };
} }
@@ -1501,6 +1516,52 @@ interface ClearIdentityLdapAuthLockoutsEvent {
}; };
} }
interface CreateIdentityOrgMembershipEvent {
type: EventType.CREATE_IDENTITY_ORG_MEMBERSHIP;
metadata: {
identityId: string;
roles: unknown;
};
}
interface UpdateIdentityOrgMembershipEvent {
type: EventType.UPDATE_IDENTITY_ORG_MEMBERSHIP;
metadata: {
identityId: string;
roles?: unknown;
};
}
interface DeleteIdentityOrgMembershipEvent {
type: EventType.DELETE_IDENTITY_ORG_MEMBERSHIP;
metadata: {
identityId: string;
};
}
interface CreateIdentityProjectMembershipEvent {
type: EventType.CREATE_IDENTITY_PROJECT_MEMBERSHIP;
metadata: {
identityId: string;
roles: unknown;
};
}
interface UpdateIdentityProjectMembershipEvent {
type: EventType.UPDATE_IDENTITY_PROJECT_MEMBERSHIP;
metadata: {
identityId: string;
roles?: unknown;
};
}
interface DeleteIdentityProjectMembershipEvent {
type: EventType.DELETE_IDENTITY_PROJECT_MEMBERSHIP;
metadata: {
identityId: string;
};
}
interface LoginIdentityOidcAuthEvent { interface LoginIdentityOidcAuthEvent {
type: EventType.LOGIN_IDENTITY_OIDC_AUTH; type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
metadata: { metadata: {
@@ -2599,6 +2660,22 @@ interface OrgAdminBypassSSOEvent {
metadata: Record<string, string>; // no metadata yet metadata: Record<string, string>; // no metadata yet
} }
interface UserLoginEvent {
type: EventType.USER_LOGIN;
metadata: {
organizationId?: string;
authProvider?: string;
};
}
interface SelectOrganizationEvent {
type: EventType.SELECT_ORGANIZATION;
metadata: {
organizationId: string;
organizationName: string;
};
}
interface CreateCertificateTemplateEstConfig { interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG; type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: { metadata: {
@@ -4197,6 +4274,12 @@ export type Event =
| GetIdentityLdapAuthEvent | GetIdentityLdapAuthEvent
| RevokeIdentityLdapAuthEvent | RevokeIdentityLdapAuthEvent
| ClearIdentityLdapAuthLockoutsEvent | ClearIdentityLdapAuthLockoutsEvent
| CreateIdentityOrgMembershipEvent
| UpdateIdentityOrgMembershipEvent
| DeleteIdentityOrgMembershipEvent
| CreateIdentityProjectMembershipEvent
| UpdateIdentityProjectMembershipEvent
| DeleteIdentityProjectMembershipEvent
| CreateEnvironmentEvent | CreateEnvironmentEvent
| GetEnvironmentEvent | GetEnvironmentEvent
| UpdateEnvironmentEvent | UpdateEnvironmentEvent
@@ -4471,4 +4554,6 @@ export type Event =
| UpdateCertificateRenewalConfigEvent | UpdateCertificateRenewalConfigEvent
| DisableCertificateRenewalConfigEvent | DisableCertificateRenewalConfigEvent
| AutomatedRenewCertificate | AutomatedRenewCertificate
| AutomatedRenewCertificateFailed; | AutomatedRenewCertificateFailed
| UserLoginEvent
| SelectOrganizationEvent;
@@ -56,6 +56,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretsLimit: 40 secretsLimit: 40
}, },
pkiEst: false, pkiEst: false,
pkiAcme: false,
enforceMfa: false, enforceMfa: false,
projectTemplates: false, projectTemplates: false,
kmip: false, kmip: false,
@@ -78,6 +78,7 @@ export type TFeatureSet = {
secretsLimit: number; secretsLimit: number;
}; };
pkiEst: boolean; pkiEst: boolean;
pkiAcme: false;
enforceMfa: boolean; enforceMfa: boolean;
projectTemplates: false; projectTemplates: false;
kmip: false; kmip: false;
@@ -171,7 +171,11 @@ const buildAdminPermissionRules = () => {
ProjectPermissionIdentityActions.Delete, ProjectPermissionIdentityActions.Delete,
ProjectPermissionIdentityActions.Read, ProjectPermissionIdentityActions.Read,
ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionIdentityActions.AssumePrivileges ProjectPermissionIdentityActions.AssumePrivileges,
ProjectPermissionIdentityActions.GetToken,
ProjectPermissionIdentityActions.CreateToken,
ProjectPermissionIdentityActions.DeleteToken,
ProjectPermissionIdentityActions.RevokeAuth
], ],
ProjectPermissionSub.Identity ProjectPermissionSub.Identity
); );
@@ -204,9 +204,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
.on(`${TableName.IdentityMetadata}.userId`, db.raw("?", [actorId])) .on(`${TableName.IdentityMetadata}.userId`, db.raw("?", [actorId]))
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
} else if (actorType === ActorType.IDENTITY) { } else if (actorType === ActorType.IDENTITY) {
void queryBuilder void queryBuilder.on(`${TableName.IdentityMetadata}.identityId`, db.raw("?", [actorId]));
.on(`${TableName.IdentityMetadata}.identityId`, db.raw("?", [actorId]))
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
} }
}) })
.where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId)
@@ -667,9 +665,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
}) })
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder.on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`);
.on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
}) })
.where(`${TableName.Membership}.scopeOrgId`, orgId) .where(`${TableName.Membership}.scopeOrgId`, orgId)
.whereNotNull(`${TableName.Membership}.actorIdentityId`) .whereNotNull(`${TableName.Membership}.actorIdentityId`)
@@ -196,7 +196,7 @@ export const permissionServiceFactory = ({
} }
if (orgId !== actorOrgId) { if (orgId !== actorOrgId) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "You are not allowed to access organization resource" });
} }
const permissionData = await permissionDAL.getPermission({ const permissionData = await permissionDAL.getPermission({
@@ -344,7 +344,7 @@ export const permissionServiceFactory = ({
}); });
if (projectDetails.orgId !== actorOrgId) { if (projectDetails.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "This project does not belong to your selected organization." });
} }
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== projectDetails.type) { if (actionProjectType !== ActionProjectType.Any && actionProjectType !== projectDetails.type) {
@@ -362,7 +362,7 @@ export const permissionServiceFactory = ({
actorId, actorId,
actorType: actor actorType: actor
}); });
if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this project" });
const permissionFromRoles = permissionData.flatMap((membership) => { const permissionFromRoles = permissionData.flatMap((membership) => {
const activeRoles = membership?.roles const activeRoles = membership?.roles
@@ -65,7 +65,11 @@ export enum ProjectPermissionIdentityActions {
Edit = "edit", Edit = "edit",
Delete = "delete", Delete = "delete",
GrantPrivileges = "grant-privileges", GrantPrivileges = "grant-privileges",
AssumePrivileges = "assume-privileges" AssumePrivileges = "assume-privileges",
RevokeAuth = "revoke-auth",
CreateToken = "create-token",
GetToken = "get-token",
DeleteToken = "delete-token"
} }
export enum ProjectPermissionMemberActions { export enum ProjectPermissionMemberActions {
@@ -76,7 +76,9 @@ export const pkiAcmeChallengeServiceFactory = ({
// challenge validation at the same time, it should be fine. // challenge validation at the same time, it should be fine.
const challengeResponse = await fetch(challengeUrl, { signal: AbortSignal.timeout(timeoutMs) }); const challengeResponse = await fetch(challengeUrl, { signal: AbortSignal.timeout(timeoutMs) });
if (challengeResponse.status !== 200) { if (challengeResponse.status !== 200) {
throw new BadRequestError({ message: "ACME challenge response is not 200" }); throw new AcmeIncorrectResponseError({
message: `ACME challenge response is not 200: ${challengeResponse.status}`
});
} }
const challengeResponseBody = await challengeResponse.text(); const challengeResponseBody = await challengeResponse.text();
const thumbprint = challenge.auth.account.publicKeyThumbprint; const thumbprint = challenge.auth.account.publicKeyThumbprint;
@@ -107,6 +109,7 @@ export const pkiAcmeChallengeServiceFactory = ({
if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) { if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) {
return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
} }
logger.error(exp, "Unknown error validating ACME challenge response");
return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
} }
} else if (exp instanceof DOMException) { } else if (exp instanceof DOMException) {
@@ -35,7 +35,7 @@ export enum AcmeErrorType {
export interface IAcmeError { export interface IAcmeError {
type: AcmeErrorType; type: AcmeErrorType;
detail: string; message: string;
status: number; status: number;
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
} }
@@ -43,7 +43,7 @@ export interface IAcmeError {
export class AcmeError extends Error implements IAcmeError { export class AcmeError extends Error implements IAcmeError {
type: AcmeErrorType; type: AcmeErrorType;
detail: string; message: string;
status: number; status: number;
@@ -53,22 +53,20 @@ export class AcmeError extends Error implements IAcmeError {
constructor({ constructor({
type, type,
detail, message,
status, status,
subproblems, subproblems,
error, error
message
}: { }: {
type: AcmeErrorType; type: AcmeErrorType;
detail: string; message: string;
status: number; status: number;
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
error?: unknown; error?: unknown;
message?: string;
}) { }) {
super(message || detail); super(message);
this.type = type; this.type = type;
this.detail = detail; this.message = message;
this.status = status; this.status = status;
this.subproblems = subproblems; this.subproblems = subproblems;
this.error = error; this.error = error;
@@ -78,7 +76,7 @@ export class AcmeError extends Error implements IAcmeError {
toAcmeResponse(): IAcmeError { toAcmeResponse(): IAcmeError {
return { return {
type: this.type, type: this.type,
detail: this.detail, message: this.message,
status: this.status, status: this.status,
subproblems: this.subproblems subproblems: this.subproblems
}; };
@@ -90,20 +88,17 @@ export class AcmeError extends Error implements IAcmeError {
*/ */
export class AcmeMalformedError extends AcmeError { export class AcmeMalformedError extends AcmeError {
constructor({ constructor({
detail = "The request message was malformed", message = "The request message was malformed",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.Malformed, type: AcmeErrorType.Malformed,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeMalformedError"; this.name = "AcmeMalformedError";
} }
@@ -114,20 +109,17 @@ export class AcmeMalformedError extends AcmeError {
*/ */
export class AcmeUnauthorizedError extends AcmeError { export class AcmeUnauthorizedError extends AcmeError {
constructor({ constructor({
detail = "The client lacks sufficient authorization", message = "The client lacks sufficient authorization",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.Unauthorized, type: AcmeErrorType.Unauthorized,
detail, message,
status: 403, status: 403,
error, error
message
}); });
this.name = "AcmeUnauthorizedError"; this.name = "AcmeUnauthorizedError";
} }
@@ -139,20 +131,17 @@ export class AcmeUnauthorizedError extends AcmeError {
*/ */
export class AcmeAccountDoesNotExistError extends AcmeError { export class AcmeAccountDoesNotExistError extends AcmeError {
constructor({ constructor({
detail = "The request specified an account that does not exist", message = "The request specified an account that does not exist",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.AccountDoesNotExist, type: AcmeErrorType.AccountDoesNotExist,
detail, message,
status: 400, status: 404,
error, error
message
}); });
this.name = "AcmeAccountDoesNotExistError"; this.name = "AcmeAccountDoesNotExistError";
} }
@@ -163,20 +152,17 @@ export class AcmeAccountDoesNotExistError extends AcmeError {
*/ */
export class AcmeBadNonceError extends AcmeError { export class AcmeBadNonceError extends AcmeError {
constructor({ constructor({
detail = "The client sent an unacceptable anti-replay nonce", message = "The client sent an unacceptable anti-replay nonce",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.BadNonce, type: AcmeErrorType.BadNonce,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeBadNonceError"; this.name = "AcmeBadNonceError";
} }
@@ -187,20 +173,17 @@ export class AcmeBadNonceError extends AcmeError {
*/ */
export class AcmeBadSignatureAlgorithmError extends AcmeError { export class AcmeBadSignatureAlgorithmError extends AcmeError {
constructor({ constructor({
detail = "The signature algorithm is invalid", message = "The signature algorithm is invalid",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.BadSignatureAlgorithm, type: AcmeErrorType.BadSignatureAlgorithm,
detail, message,
status: 401, status: 401,
error, error
message
}); });
this.name = "AcmeBadSignatureAlgorithmError"; this.name = "AcmeBadSignatureAlgorithmError";
} }
@@ -211,20 +194,17 @@ export class AcmeBadSignatureAlgorithmError extends AcmeError {
*/ */
export class AcmeBadPublicKeyError extends AcmeError { export class AcmeBadPublicKeyError extends AcmeError {
constructor({ constructor({
detail = "The public key is not acceptable", message = "The public key is not acceptable",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.BadPublicKey, type: AcmeErrorType.BadPublicKey,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeBadPublicKeyError"; this.name = "AcmeBadPublicKeyError";
} }
@@ -235,20 +215,17 @@ export class AcmeBadPublicKeyError extends AcmeError {
*/ */
export class AcmeBadCsrError extends AcmeError { export class AcmeBadCsrError extends AcmeError {
constructor({ constructor({
detail = "The CSR is unacceptable", message = "The CSR is unacceptable",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.BadCsr, type: AcmeErrorType.BadCsr,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeBadCsrError"; this.name = "AcmeBadCsrError";
} }
@@ -260,20 +237,17 @@ export class AcmeBadCsrError extends AcmeError {
*/ */
export class AcmeBadRevocationReasonError extends AcmeError { export class AcmeBadRevocationReasonError extends AcmeError {
constructor({ constructor({
detail = "The revocation reason provided is not allowed", message = "The revocation reason provided is not allowed",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.BadRevocationReason, type: AcmeErrorType.BadRevocationReason,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeBadRevocationReasonError"; this.name = "AcmeBadRevocationReasonError";
} }
@@ -284,20 +258,17 @@ export class AcmeBadRevocationReasonError extends AcmeError {
*/ */
export class AcmeRateLimitedError extends AcmeError { export class AcmeRateLimitedError extends AcmeError {
constructor({ constructor({
detail = "The client has exceeded a rate limit", message = "The client has exceeded a rate limit",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.RateLimited, type: AcmeErrorType.RateLimited,
detail, message,
status: 429, status: 429,
error, error
message
}); });
this.name = "AcmeRateLimitedError"; this.name = "AcmeRateLimitedError";
} }
@@ -309,23 +280,20 @@ export class AcmeRateLimitedError extends AcmeError {
*/ */
export class AcmeRejectedIdentifierError extends AcmeError { export class AcmeRejectedIdentifierError extends AcmeError {
constructor({ constructor({
detail = "The server will not issue certificates for the identifier", message = "The server will not issue certificates for the identifier",
subproblems, subproblems,
error, error
message
}: { }: {
detail?: string; message?: string;
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
error?: unknown; error?: unknown;
message?: string;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.RejectedIdentifier, type: AcmeErrorType.RejectedIdentifier,
detail, message,
status: 400, status: 400,
subproblems, subproblems,
error, error
message
}); });
this.name = "AcmeRejectedIdentifierError"; this.name = "AcmeRejectedIdentifierError";
} }
@@ -336,20 +304,17 @@ export class AcmeRejectedIdentifierError extends AcmeError {
*/ */
export class AcmeServerInternalError extends AcmeError { export class AcmeServerInternalError extends AcmeError {
constructor({ constructor({
detail = "An internal error occurred", message = "An internal error occurred",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.ServerInternal, type: AcmeErrorType.ServerInternal,
detail, message,
status: 500, status: 500,
error, error
message
}); });
this.name = "AcmeServerInternalError"; this.name = "AcmeServerInternalError";
} }
@@ -360,20 +325,17 @@ export class AcmeServerInternalError extends AcmeError {
*/ */
export class AcmeUnsupportedContactError extends AcmeError { export class AcmeUnsupportedContactError extends AcmeError {
constructor({ constructor({
detail = "A contact URL is of an unsupported type", message = "A contact URL is of an unsupported type",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.UnsupportedContact, type: AcmeErrorType.UnsupportedContact,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeUnsupportedContactError"; this.name = "AcmeUnsupportedContactError";
} }
@@ -385,20 +347,17 @@ export class AcmeUnsupportedContactError extends AcmeError {
*/ */
export class AcmeUnsupportedIdentifierError extends AcmeError { export class AcmeUnsupportedIdentifierError extends AcmeError {
constructor({ constructor({
detail = "An identifier is of an unsupported type", message = "An identifier is of an unsupported type",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.UnsupportedIdentifier, type: AcmeErrorType.UnsupportedIdentifier,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeUnsupportedIdentifierError"; this.name = "AcmeUnsupportedIdentifierError";
} }
@@ -412,22 +371,19 @@ export class AcmeUserActionRequiredError extends AcmeError {
instance?: string; instance?: string;
constructor({ constructor({
detail = "Visit the instance URL and take actions specified there", message = "Visit the instance URL and take actions specified there",
instance, instance,
error, error
message
}: { }: {
detail?: string; message?: string;
instance?: string; instance?: string;
error?: unknown; error?: unknown;
message?: string;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.UserActionRequired, type: AcmeErrorType.UserActionRequired,
detail, message,
status: 403, status: 403,
error, error
message
}); });
this.instance = instance; this.instance = instance;
this.name = "AcmeUserActionRequiredError"; this.name = "AcmeUserActionRequiredError";
@@ -446,20 +402,17 @@ export class AcmeUserActionRequiredError extends AcmeError {
*/ */
export class AcmeIncorrectResponseError extends AcmeError { export class AcmeIncorrectResponseError extends AcmeError {
constructor({ constructor({
detail = "The response is incorrect", message = "The response is incorrect",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.IncorrectResponse, type: AcmeErrorType.IncorrectResponse,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeIncorrectResponseError"; this.name = "AcmeIncorrectResponseError";
} }
@@ -470,20 +423,17 @@ export class AcmeIncorrectResponseError extends AcmeError {
*/ */
export class AcmeConnectionError extends AcmeError { export class AcmeConnectionError extends AcmeError {
constructor({ constructor({
detail = "A connection error occurred", message = "A connection error occurred",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.Connection, type: AcmeErrorType.Connection,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeConnectionError"; this.name = "AcmeConnectionError";
} }
@@ -491,20 +441,17 @@ export class AcmeConnectionError extends AcmeError {
export class AcmeDnsFailureError extends AcmeError { export class AcmeDnsFailureError extends AcmeError {
constructor({ constructor({
detail = "Hostname could not be resolved (DNS failure)", message = "Hostname could not be resolved (DNS failure)",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.DNS, type: AcmeErrorType.DNS,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeDnsFailureError"; this.name = "AcmeDnsFailureError";
} }
@@ -512,20 +459,17 @@ export class AcmeDnsFailureError extends AcmeError {
export class AcmeOrderNotReadyError extends AcmeError { export class AcmeOrderNotReadyError extends AcmeError {
constructor({ constructor({
detail = "The order is not ready", message = "The order is not ready",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.OrderNotReady, type: AcmeErrorType.OrderNotReady,
detail, message,
status: 403, status: 400,
error, error
message
}); });
this.name = "AcmeOrderNotReadyError"; this.name = "AcmeOrderNotReadyError";
} }
@@ -533,20 +477,17 @@ export class AcmeOrderNotReadyError extends AcmeError {
export class AcmeBadCSRError extends AcmeError { export class AcmeBadCSRError extends AcmeError {
constructor({ constructor({
detail = "The CSR is unacceptable", message = "The CSR is unacceptable",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.BadCsr, type: AcmeErrorType.BadCsr,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeBadCSRError"; this.name = "AcmeBadCSRError";
} }
@@ -554,20 +495,17 @@ export class AcmeBadCSRError extends AcmeError {
export class AcmeExternalAccountRequiredError extends AcmeError { export class AcmeExternalAccountRequiredError extends AcmeError {
constructor({ constructor({
detail = "External account binding is required", message = "External account binding is required",
error, error
message
}: { }: {
detail?: string;
error?: unknown;
message?: string; message?: string;
error?: unknown;
} = {}) { } = {}) {
super({ super({
type: AcmeErrorType.ExternalAccountRequired, type: AcmeErrorType.ExternalAccountRequired,
detail, message,
status: 400, status: 400,
error, error
message
}); });
this.name = "AcmeExternalAccountRequiredError"; this.name = "AcmeExternalAccountRequiredError";
} }
@@ -1,8 +1,9 @@
import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { AcmeMalformedError } from "./pki-acme-errors"; import { AcmeAccountDoesNotExistError } from "./pki-acme-errors";
export const buildUrl = (profileId: string, path: string): string => { export const buildUrl = (profileId: string, path: string): string => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -13,7 +14,14 @@ export const buildUrl = (profileId: string, path: string): string => {
export const extractAccountIdFromKid = (kid: string, profileId: string): string => { export const extractAccountIdFromKid = (kid: string, profileId: string): string => {
const kidPrefix = buildUrl(profileId, "/accounts/"); const kidPrefix = buildUrl(profileId, "/accounts/");
if (!kid.startsWith(kidPrefix)) { if (!kid.startsWith(kidPrefix)) {
throw new AcmeMalformedError({ detail: "KID must start with the profile account URL" }); throw new AcmeAccountDoesNotExistError({ message: "KID must start with the profile account URL" });
} }
return z.string().uuid().parse(kid.slice(kidPrefix.length)); return z.string().uuid().parse(kid.slice(kidPrefix.length));
}; };
export const validateDnsIdentifier = (identifier: string): boolean => {
// DNS label pattern: 1-63 chars, alphanumeric or hyphen, but not starting or ending with hyphen
const labelPattern = new RE2(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/);
const labels = identifier.split(".");
return labels.every((label) => label.length >= 1 && label.length <= 63 && labelPattern.test(label));
};
@@ -1,4 +1,3 @@
import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
export enum AcmeIdentifierType { export enum AcmeIdentifierType {
@@ -88,13 +87,8 @@ export const CreateAcmeAccountResponseSchema = z.object({
export const CreateAcmeOrderBodySchema = z.object({ export const CreateAcmeOrderBodySchema = z.object({
identifiers: z.array( identifiers: z.array(
z.object({ z.object({
type: z.enum(Object.values(AcmeIdentifierType) as [string, ...string[]]), type: z.string(),
value: z.string().refine((val) => { value: z.string()
// DNS label pattern: 1-63 chars, alphanumeric or hyphen, but not starting or ending with hyphen
const labelPattern = new RE2(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/);
const labels = val.split(".");
return labels.every((label) => label.length >= 1 && label.length <= 63 && labelPattern.test(label));
}, "Invalid DNS identifier")
}) })
), ),
notBefore: z.string().optional(), notBefore: z.string().optional(),
@@ -12,12 +12,26 @@ import { z, ZodError } from "zod";
import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts";
import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isPrivateIp } from "@app/lib/ip/ipRange"; import { isPrivateIp } from "@app/lib/ip/ipRange";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertKeyUsage,
CertSubjectAlternativeNameType
} from "@app/services/certificate/certificate-types";
import { orderCertificate } from "@app/services/certificate-authority/acme/acme-certificate-authority-fns";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal";
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { import {
EnrollmentType, EnrollmentType,
@@ -28,6 +42,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal";
import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal";
import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal";
@@ -44,7 +59,7 @@ import {
AcmeUnauthorizedError, AcmeUnauthorizedError,
AcmeUnsupportedIdentifierError AcmeUnsupportedIdentifierError
} from "./pki-acme-errors"; } from "./pki-acme-errors";
import { buildUrl, extractAccountIdFromKid } from "./pki-acme-fns"; import { buildUrl, extractAccountIdFromKid, validateDnsIdentifier } from "./pki-acme-fns";
import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal";
import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal";
import { import {
@@ -77,9 +92,14 @@ import {
} from "./pki-acme-types"; } from "./pki-acme-types";
type TPkiAcmeServiceFactoryDep = { type TPkiAcmeServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">; certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
acmeAccountDAL: Pick< acmeAccountDAL: Pick<
TPkiAcmeAccountDALFactory, TPkiAcmeAccountDALFactory,
"findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create"
@@ -100,15 +120,24 @@ type TPkiAcmeServiceFactoryDep = {
"create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation" "create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation"
>; >;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">; keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<
TKmsServiceFactory,
"decryptWithKmsKey" | "generateKmsKey" | "encryptWithKmsKey" | "createCipherPairWithDataKey"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">; certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
acmeChallengeService: TPkiAcmeChallengeServiceFactory; acmeChallengeService: TPkiAcmeChallengeServiceFactory;
}; };
export const pkiAcmeServiceFactory = ({ export const pkiAcmeServiceFactory = ({
projectDAL, projectDAL,
appConnectionDAL,
certificateDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateProfileDAL, certificateProfileDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL,
acmeAccountDAL, acmeAccountDAL,
acmeOrderDAL, acmeOrderDAL,
acmeAuthDAL, acmeAuthDAL,
@@ -116,6 +145,7 @@ export const pkiAcmeServiceFactory = ({
acmeChallengeDAL, acmeChallengeDAL,
keyStore, keyStore,
kmsService, kmsService,
licenseService,
certificateV3Service, certificateV3Service,
acmeChallengeService acmeChallengeService
}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
@@ -127,6 +157,12 @@ export const pkiAcmeServiceFactory = ({
if (profile.enrollmentType !== EnrollmentType.ACME) { if (profile.enrollmentType !== EnrollmentType.ACME) {
throw new NotFoundError({ message: "Certificate profile is not configured for ACME enrollment" }); throw new NotFoundError({ message: "Certificate profile is not configured for ACME enrollment" });
} }
const orgLicensePlan = await licenseService.getPlan(profile.project!.orgId);
if (!orgLicensePlan.pkiAcme) {
throw new AcmeUnauthorizedError({
message: "Failed to validate ACME profile: Plan restriction. Upgrade plan to continue"
});
}
return profile; return profile;
}; };
@@ -148,7 +184,7 @@ export const pkiAcmeServiceFactory = ({
try { try {
result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => { result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => {
if (protectedHeader === undefined) { if (protectedHeader === undefined) {
throw new AcmeMalformedError({ detail: "Protected header is required" }); throw new AcmeMalformedError({ message: "Protected header is required" });
} }
const jwk = await getJWK(protectedHeader); const jwk = await getJWK(protectedHeader);
const key = await importJWK(jwk, protectedHeader.alg); const key = await importJWK(jwk, protectedHeader.alg);
@@ -159,28 +195,35 @@ export const pkiAcmeServiceFactory = ({
throw error; throw error;
} }
if (error instanceof ZodError) { if (error instanceof ZodError) {
throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); throw new AcmeMalformedError({ message: `Invalid JWS payload: ${error.message}` });
} }
if (error instanceof errors.JWSSignatureVerificationFailed) { if (error instanceof errors.JWSSignatureVerificationFailed) {
throw new AcmeBadPublicKeyError({ detail: "Invalid JWS payload" }); throw new AcmeBadPublicKeyError({ message: "Invalid JWS payload" });
} }
logger.error(error, "Unexpected error while verifying JWS payload"); logger.error(error, "Unexpected error while verifying JWS payload");
throw new AcmeServerInternalError({ detail: "Failed to verify JWS payload" }); throw new AcmeMalformedError({ message: "Failed to verify JWS payload" });
} }
const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result;
try { try {
const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader);
const parsedUrl = (() => {
try {
return new URL(protectedHeader.url);
} catch (error) {
throw new AcmeMalformedError({ message: "Invalid URL in the protected header" });
}
})();
// Validate the URL // Validate the URL
if (new URL(protectedHeader.url).href !== url.href) { if (parsedUrl.href !== url.href) {
throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" }); throw new AcmeMalformedError({ message: "URL mismatch in the protected header" });
} }
// Consume the nonce // Consume the nonce
if (!protectedHeader.nonce) { if (!protectedHeader.nonce) {
throw new AcmeMalformedError({ detail: "Nonce is required in the protected header" }); throw new AcmeMalformedError({ message: "Nonce is required in the protected header" });
} }
const deleted = await keyStore.deleteItem(KeyStorePrefixes.PkiAcmeNonce(protectedHeader.nonce)); const deleted = await keyStore.deleteItem(KeyStorePrefixes.PkiAcmeNonce(protectedHeader.nonce));
if (deleted !== 1) { if (deleted !== 1) {
throw new AcmeBadNonceError({ detail: "Invalid nonce" }); throw new AcmeBadNonceError({ message: "Invalid nonce" });
} }
// Parse the payload // Parse the payload
@@ -196,10 +239,10 @@ export const pkiAcmeServiceFactory = ({
throw error; throw error;
} }
if (error instanceof ZodError) { if (error instanceof ZodError) {
throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); throw new AcmeMalformedError({ message: `Invalid JWS payload: ${error.message}` });
} }
logger.error(error, "Unexpected error while parsing JWS payload"); logger.error(error, "Unexpected error while parsing JWS payload");
throw new AcmeServerInternalError({ detail: "Failed to verify JWS payload" }); throw new AcmeMalformedError({ message: "Failed to verify JWS payload" });
} }
}; };
@@ -215,7 +258,7 @@ export const pkiAcmeServiceFactory = ({
rawJwsPayload, rawJwsPayload,
getJWK: async (protectedHeader) => { getJWK: async (protectedHeader) => {
if (!protectedHeader.jwk) { if (!protectedHeader.jwk) {
throw new AcmeMalformedError({ detail: "JWK is required in the protected header" }); throw new AcmeMalformedError({ message: "JWK is required in the protected header" });
} }
return protectedHeader.jwk as unknown as JsonWebKey; return protectedHeader.jwk as unknown as JsonWebKey;
}, },
@@ -246,18 +289,18 @@ export const pkiAcmeServiceFactory = ({
rawJwsPayload, rawJwsPayload,
getJWK: async (protectedHeader) => { getJWK: async (protectedHeader) => {
if (!protectedHeader.kid) { if (!protectedHeader.kid) {
throw new AcmeMalformedError({ detail: "KID is required in the protected header" }); throw new AcmeMalformedError({ message: "KID is required in the protected header" });
} }
const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId); const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId);
if (expectedAccountId && accountId !== expectedAccountId) { if (expectedAccountId && accountId !== expectedAccountId) {
throw new NotFoundError({ message: "ACME resource not found" }); throw new AcmeAccountDoesNotExistError({ message: "ACME resource not found" });
} }
const account = await acmeAccountDAL.findByProjectIdAndAccountId(profile.id, accountId); const account = await acmeAccountDAL.findByProjectIdAndAccountId(profile.id, accountId);
if (!account) { if (!account) {
throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" });
} }
if (account.alg !== protectedHeader.alg) { if (account.alg !== protectedHeader.alg) {
throw new AcmeMalformedError({ detail: "ACME account algorithm mismatch" }); throw new AcmeMalformedError({ message: "ACME account algorithm mismatch" });
} }
return account.publicKey as JsonWebKey; return account.publicKey as JsonWebKey;
}, },
@@ -344,7 +387,7 @@ export const pkiAcmeServiceFactory = ({
}): Promise<TAcmeResponse<TCreateAcmeAccountResponse>> => { }): Promise<TAcmeResponse<TCreateAcmeAccountResponse>> => {
const profile = await validateAcmeProfile(profileId); const profile = await validateAcmeProfile(profileId);
if (!externalAccountBinding) { if (!externalAccountBinding) {
throw new AcmeExternalAccountRequiredError({ detail: "External account binding is required" }); throw new AcmeExternalAccountRequiredError({ message: "External account binding is required" });
} }
const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256"); const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256");
@@ -363,26 +406,28 @@ export const pkiAcmeServiceFactory = ({
return { eabPayload: result.payload, eabProtectedHeader: result.protectedHeader }; return { eabPayload: result.payload, eabProtectedHeader: result.protectedHeader };
} catch (error) { } catch (error) {
if (error instanceof errors.JWSSignatureVerificationFailed) { if (error instanceof errors.JWSSignatureVerificationFailed) {
throw new AcmeMalformedError({ detail: "Invalid external account binding JWS signature" }); throw new AcmeExternalAccountRequiredError({ message: "Invalid external account binding JWS signature" });
} }
logger.error(error, "Unexpected error while verifying EAB JWS signature"); logger.error(error, "Unexpected error while verifying EAB JWS signature");
throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS signature" }); throw new AcmeServerInternalError({ message: "Failed to verify EAB JWS signature" });
} }
})(); })();
const { alg: eabAlg, kid: eabKid } = eabProtectedHeader!; const { alg: eabAlg, kid: eabKid } = eabProtectedHeader!;
if (!["HS256", "HS384", "HS512"].includes(eabAlg!)) { if (!["HS256", "HS384", "HS512"].includes(eabAlg!)) {
throw new AcmeMalformedError({ detail: "Invalid algorithm for external account binding JWS payload" }); throw new AcmeExternalAccountRequiredError({
message: "Invalid algorithm for external account binding JWS payload"
});
} }
// Make sure the KID in the EAB payload matches the profile ID // Make sure the KID in the EAB payload matches the profile ID
if (eabKid !== profile.id) { if (eabKid !== profile.id) {
throw new UnauthorizedError({ message: "External account binding KID mismatch" }); throw new AcmeExternalAccountRequiredError({ message: "External account binding KID mismatch" });
} }
// Make sure the URL matches the expected URL // Make sure the URL matches the expected URL
const url = eabProtectedHeader!.url!; const url = eabProtectedHeader!.url!;
if (url !== buildUrl(profile.id, "/new-account")) { if (url !== buildUrl(profile.id, "/new-account")) {
throw new UnauthorizedError({ message: "External account binding URL mismatch" }); throw new AcmeExternalAccountRequiredError({ message: "External account binding URL mismatch" });
} }
// Make sure the JWK in the EAB payload matches the one provided in the outer JWS payload // Make sure the JWK in the EAB payload matches the one provided in the outer JWS payload
@@ -481,6 +526,21 @@ export const pkiAcmeServiceFactory = ({
// TODO: check the identifiers and see if are they even allowed for this profile. // TODO: check the identifiers and see if are they even allowed for this profile.
// if not, we may be able to reject it early with an unsupportedIdentifier error. // if not, we may be able to reject it early with an unsupportedIdentifier error.
// TODO: ideally, we should return an error with subproblems if we have multiple unsupported identifiers
if (payload.identifiers.some((identifier) => identifier.type !== AcmeIdentifierType.DNS)) {
throw new AcmeUnsupportedIdentifierError({ message: "Only DNS identifiers are supported" });
}
if (
payload.identifiers.some(
(identifier) =>
!validateDnsIdentifier(identifier.value) ||
isPrivateIp(identifier.value) ||
(!getConfig().isDevelopmentMode && identifier.value.toLowerCase() === "localhost")
)
) {
throw new AcmeUnsupportedIdentifierError({ message: "Invalid DNS identifier" });
}
const order = await acmeOrderDAL.transaction(async (tx) => { const order = await acmeOrderDAL.transaction(async (tx) => {
const account = (await acmeAccountDAL.findByProjectIdAndAccountId(profileId, accountId))!; const account = (await acmeAccountDAL.findByProjectIdAndAccountId(profileId, accountId))!;
const createdOrder = await acmeOrderDAL.create( const createdOrder = await acmeOrderDAL.create(
@@ -497,10 +557,10 @@ export const pkiAcmeServiceFactory = ({
const authorizations: TPkiAcmeAuths[] = await Promise.all( const authorizations: TPkiAcmeAuths[] = await Promise.all(
payload.identifiers.map(async (identifier) => { payload.identifiers.map(async (identifier) => {
if (identifier.type !== AcmeIdentifierType.DNS) { if (identifier.type !== AcmeIdentifierType.DNS) {
throw new AcmeUnsupportedIdentifierError({ detail: "Only DNS identifiers are supported" }); throw new AcmeUnsupportedIdentifierError({ message: "Only DNS identifiers are supported" });
} }
if (isPrivateIp(identifier.value)) { if (isPrivateIp(identifier.value)) {
throw new AcmeUnsupportedIdentifierError({ detail: "Private IP addresses are not allowed" }); throw new AcmeUnsupportedIdentifierError({ message: "Private IP addresses are not allowed" });
} }
const auth = await acmeAuthDAL.create( const auth = await acmeAuthDAL.create(
{ {
@@ -588,6 +648,7 @@ export const pkiAcmeServiceFactory = ({
orderId: string; orderId: string;
payload: TFinalizeAcmeOrderPayload; payload: TFinalizeAcmeOrderPayload;
}): Promise<TAcmeResponse<TAcmeOrderResource>> => { }): Promise<TAcmeResponse<TAcmeOrderResource>> => {
const profile = (await certificateProfileDAL.findByIdWithConfigs(profileId))!;
let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId); let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId);
if (!order) { if (!order) {
throw new NotFoundError({ message: "ACME order not found" }); throw new NotFoundError({ message: "ACME order not found" });
@@ -603,28 +664,100 @@ export const pkiAcmeServiceFactory = ({
if (finalizingOrder.expiresAt < new Date()) { if (finalizingOrder.expiresAt < new Date()) {
throw new AcmeOrderNotReadyError({ message: "ACME order has expired" }); throw new AcmeOrderNotReadyError({ message: "ACME order has expired" });
} }
const { csr } = payload; const { csr } = payload;
// Check and validate the CSR
const certificateRequest = extractCertificateRequestFromCSR(csr);
if (!certificateRequest.commonName) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name is required" });
}
if (
certificateRequest.subjectAlternativeNames?.some(
(san) => san.type !== CertSubjectAlternativeNameType.DNS_NAME
)
) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Only DNS subject alternative names are supported" });
}
const orderWithAuthorizations = (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(
accountId,
orderId,
tx
))!;
const csrIdentifierValues = new Set(
(certificateRequest.subjectAlternativeNames ?? [])
.map((san) => san.value.toLowerCase())
.concat([certificateRequest.commonName.toLowerCase()])
);
if (
csrIdentifierValues.size !== orderWithAuthorizations.authorizations.length ||
!orderWithAuthorizations.authorizations.every((auth) =>
csrIdentifierValues.has(auth.identifierValue.toLowerCase())
)
) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name + SANs mismatch with order identifiers" });
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
let errorToReturn: Error | undefined; let errorToReturn: Error | undefined;
try { try {
const { certificateId } = await certificateV3Service.signCertificateFromProfile({ const { certificateId } = await (async () => {
actor: ActorType.ACME_ACCOUNT, if (caType === CaType.INTERNAL) {
actorId: accountId, const result = await certificateV3Service.signCertificateFromProfile({
actorAuthMethod: null, actor: ActorType.ACME_ACCOUNT,
actorOrgId, actorId: accountId,
profileId, actorAuthMethod: null,
csr, actorOrgId,
notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined, profileId,
notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined, csr,
validity: !finalizingOrder.notAfter notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined,
? { notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined,
// TODO: read config from the profile to get the expiration time instead validity: !finalizingOrder.notAfter
ttl: (24 * 60 * 60 * 1000).toString() ? {
} // 47 days, the default TTL comes with Let's Encrypt
: // ttl is not used if notAfter is provided // TODO: read config from the profile to get the expiration time instead
({ ttl: "0" } as const), ttl: `${47}d`
enrollmentType: EnrollmentType.ACME }
}); : // ttl is not used if notAfter is provided
// TODO: associate the certificate with the order ({ ttl: "0d" } as const),
enrollmentType: EnrollmentType.ACME
});
return { certificateId: result.certificateId };
}
const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!;
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const csrPem = csrObj.toString("pem");
// TODO: for internal CA, we rely on the internal certificate authority service to check CSR against the template
// we should check the CSR against the template here
// TODO: this is pretty slow, and we are holding the transaction open for a long time,
// we should queue the certificate issuance to a background job instead
const cert = await orderCertificate(
{
caId: certificateAuthority!.id,
commonName: certificateRequest.commonName!,
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
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT, CertKeyUsage.KEY_AGREEMENT],
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH]
},
{
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
}
);
return { certificateId: cert.id };
})();
await acmeOrderDAL.updateById( await acmeOrderDAL.updateById(
orderId, orderId,
{ {
@@ -647,9 +780,9 @@ export const pkiAcmeServiceFactory = ({
logger.error(exp, "Failed to sign certificate"); logger.error(exp, "Failed to sign certificate");
// TODO: audit log the error // TODO: audit log the error
if (exp instanceof BadRequestError) { if (exp instanceof BadRequestError) {
errorToReturn = new AcmeBadCSRError({ detail: `Invalid CSR: ${exp.message}` }); errorToReturn = new AcmeBadCSRError({ message: `Invalid CSR: ${exp.message}` });
} else { } else {
errorToReturn = new AcmeServerInternalError({ detail: "Failed to sign certificate with internal error" }); errorToReturn = new AcmeServerInternalError({ message: "Failed to sign certificate with internal error" });
} }
} }
return { return {
@@ -3,6 +3,7 @@ import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs"; import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { import {
BaseSecretSyncSchema, BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema, GenericCreateSecretSyncFieldsSchema,
@@ -25,10 +26,12 @@ const ChefSyncDestinationConfigSchema = z.object({
const ChefSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; const ChefSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const ChefSyncSchema = BaseSecretSyncSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({ export const ChefSyncSchema = BaseSecretSyncSchema(SecretSync.Chef, ChefSyncOptionsConfig)
destination: z.literal(SecretSync.Chef), .extend({
destinationConfig: ChefSyncDestinationConfigSchema destination: z.literal(SecretSync.Chef),
}); destinationConfig: ChefSyncDestinationConfigSchema
})
.describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.Chef] }));
export const CreateChefSyncSchema = GenericCreateSecretSyncFieldsSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({ export const CreateChefSyncSchema = GenericCreateSecretSyncFieldsSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({
destinationConfig: ChefSyncDestinationConfigSchema destinationConfig: ChefSyncDestinationConfigSchema
@@ -38,10 +41,12 @@ export const UpdateChefSyncSchema = GenericUpdateSecretSyncFieldsSchema(SecretSy
destinationConfig: ChefSyncDestinationConfigSchema.optional() destinationConfig: ChefSyncDestinationConfigSchema.optional()
}); });
export const ChefSyncListItemSchema = z.object({ export const ChefSyncListItemSchema = z
name: z.literal("Chef"), .object({
connection: z.literal(AppConnection.Chef), name: z.literal("Chef"),
destination: z.literal(SecretSync.Chef), connection: z.literal(AppConnection.Chef),
canImportSecrets: z.literal(true), destination: z.literal(SecretSync.Chef),
enterprise: z.boolean() canImportSecrets: z.literal(true),
}); enterprise: z.boolean()
})
.describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.Chef] }));
@@ -4,6 +4,7 @@ import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs"; import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { import {
BaseSecretSyncSchema, BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema, GenericCreateSecretSyncFieldsSchema,
@@ -43,10 +44,12 @@ const OCIVaultSyncDestinationConfigSchema = z.object({
const OCIVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; const OCIVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const OCIVaultSyncSchema = BaseSecretSyncSchema(SecretSync.OCIVault, OCIVaultSyncOptionsConfig).extend({ export const OCIVaultSyncSchema = BaseSecretSyncSchema(SecretSync.OCIVault, OCIVaultSyncOptionsConfig)
destination: z.literal(SecretSync.OCIVault), .extend({
destinationConfig: OCIVaultSyncDestinationConfigSchema destination: z.literal(SecretSync.OCIVault),
}); destinationConfig: OCIVaultSyncDestinationConfigSchema
})
.describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.OCIVault] }));
export const CreateOCIVaultSyncSchema = GenericCreateSecretSyncFieldsSchema( export const CreateOCIVaultSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.OCIVault, SecretSync.OCIVault,
@@ -62,10 +65,12 @@ export const UpdateOCIVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema(
destinationConfig: OCIVaultSyncDestinationConfigSchema.optional() destinationConfig: OCIVaultSyncDestinationConfigSchema.optional()
}); });
export const OCIVaultSyncListItemSchema = z.object({ export const OCIVaultSyncListItemSchema = z
name: z.literal("OCI Vault"), .object({
connection: z.literal(AppConnection.OCI), name: z.literal("OCI Vault"),
destination: z.literal(SecretSync.OCIVault), connection: z.literal(AppConnection.OCI),
canImportSecrets: z.literal(true), destination: z.literal(SecretSync.OCIVault),
enterprise: z.boolean() canImportSecrets: z.literal(true),
}); enterprise: z.boolean()
})
.describe(JSON.stringify({ title: SECRET_SYNC_NAME_MAP[SecretSync.OCIVault] }));
+104 -3
View File
@@ -33,11 +33,13 @@ export enum ApiDocsTags {
LdapAuth = "LDAP Auth", LdapAuth = "LDAP Auth",
Groups = "Groups", Groups = "Groups",
Organizations = "Organizations", Organizations = "Organizations",
OrgIdentityMembership = "Organization Identity Membership",
SubOrganizations = "Sub Organizations", SubOrganizations = "Sub Organizations",
Projects = "Projects", Projects = "Projects",
ProjectUsers = "Project Users", ProjectUsers = "Project Users",
ProjectGroups = "Project Groups", ProjectGroups = "Project Groups",
ProjectIdentities = "Project Identities", ProjectIdentities = "Project Identities",
IdentityProjectMembership = "Project Identity Membership",
ProjectRoles = "Project Roles", ProjectRoles = "Project Roles",
ProjectTemplates = "Project Templates", ProjectTemplates = "Project Templates",
Environments = "Environments", Environments = "Environments",
@@ -122,13 +124,15 @@ export const IDENTITIES = {
name: "The name of the identity to create.", name: "The name of the identity to create.",
organizationId: "The organization ID to which the identity belongs.", organizationId: "The organization ID to which the identity belongs.",
role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'.", role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'.",
hasDeleteProtection: "Prevents deletion of the identity when enabled." hasDeleteProtection: "Prevents deletion of the identity when enabled.",
metadata: "An optional array of key-value pairs to attach to the identity."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the machine identity to update.", identityId: "The ID of the machine identity to update.",
name: "The new name of the identity.", name: "The new name of the identity.",
role: "The new role of the identity.", role: "The new role of the identity.",
hasDeleteProtection: "Prevents deletion of the identity when enabled." hasDeleteProtection: "Prevents deletion of the identity when enabled.",
metadata: "An optional array of key-value pairs to attach to the identity."
}, },
DELETE: { DELETE: {
identityId: "The ID of the machine identity to delete." identityId: "The ID of the machine identity to delete."
@@ -138,7 +142,10 @@ export const IDENTITIES = {
orgId: "The ID of the org of the identity" orgId: "The ID of the org of the identity"
}, },
LIST: { LIST: {
orgId: "The ID of the organization to list identities." orgId: "The ID of the organization to list identities.",
search: "The text string that identity names will be filtered by.",
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
limit: "The number of identities to return."
}, },
SEARCH: { SEARCH: {
search: { search: {
@@ -719,6 +726,50 @@ export const ORGANIZATIONS = {
} }
} as const; } as const;
export const ORG_IDENTITY_MEMBERSHIP = {
CREATE_IDENTITY_MEMBERSHIP: {
identityId: "The ID of the machine identity to create the membership for.",
roles: {
description: "A list of role slugs to assign to the identity organization membership.",
role: "The role slug to assign to the newly created identity organization membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
UPDATE_IDENTITY_MEMBERSHIP: {
identityId: "The ID of the machine identity to update the membership for.",
roles: {
description: "A list of role slugs to assign to the identity organization membership.",
role: "The role slug to assign to the identity organization membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
DELETE_IDENTITY_MEMBERSHIP: {
identityId: "The ID of the machine identity to delete the membership from."
},
LIST_IDENTITY_MEMBERSHIPS: {
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
identityName: "",
roles: "The role slugs to filter identity memberships by."
},
GET_IDENTITY_MEMBERSHIP_BY_ID: {
identityId: "The ID of the machine identity to get the membership for."
},
LIST_AVAILABLE_IDENTITIES: {
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
limit: "The number of identities to return.",
identityName: "The text string that identity membership names will be filtered by."
}
} as const;
export const SUB_ORGANIZATIONS = { export const SUB_ORGANIZATIONS = {
CREATE: { CREATE: {
name: "The name of the sub organization to create." name: "The name of the sub organization to create."
@@ -907,6 +958,56 @@ export const PROJECT_IDENTITIES = {
} }
}; };
export const PROJECT_IDENTITY_MEMBERSHIP = {
CREATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to create the identity membership for.",
identityId: "The ID of the machine identity to create the membership for.",
roles: {
description: "A list of role slugs to assign to the identity project membership.",
role: "The role slug to assign to the newly created identity project membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
UPDATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to update the identity membership for.",
identityId: "The ID of the machine identity to update the membership for.",
roles: {
description: "A list of role slugs to assign to the identity project membership.",
role: "The role slug to assign to the identity project membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
DELETE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to delete the identity membership from.",
identityId: "The ID of the machine identity to delete the membership from."
},
LIST_IDENTITY_MEMBERSHIPS: {
projectId: "The ID of the project to list identity memberships from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
identityName: "The text string that identity membership names will be filtered by.",
roles: "The role slugs to filter identity memberships by."
},
GET_IDENTITY_MEMBERSHIP_BY_ID: {
projectId: "The ID of the project to get the identity membership for.",
identityId: "The ID of the machine identity to get the membership for."
},
LIST_AVAILABLE_IDENTITIES: {
projectId: "The ID of the project to list available identities for.",
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
limit: "The number of identities to return.",
identityName: "The text string that identity membership names will be filtered by."
}
} as const;
export const ENVIRONMENTS = { export const ENVIRONMENTS = {
CREATE: { CREATE: {
projectId: "The ID of the project to create the environment in.", projectId: "The ID of the project to create the environment in.",
+3 -5
View File
@@ -106,11 +106,9 @@ const envSchema = z
HTTPS_ENABLED: zodStrBool, HTTPS_ENABLED: zodStrBool,
ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
// Note: The ACME feature is still in development and is not yet ready for production. BDD_NOCK_API_ENABLED: zodStrBool.default("false").optional(),
// This is the feature flag to enable/disable the ACME feature.
// It's not intended to be used by users outside of the development team yet.
ACME_FEATURE_ENABLED: zodStrBool.default("false").optional(),
ACME_DEVELOPMENT_MODE: zodStrBool.default("false").optional(), ACME_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
ACME_SKIP_UPSTREAM_VALIDATION: zodStrBool.default("false").optional(),
ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES: zpStr( ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES: zpStr(
z z
.string() .string()
@@ -399,10 +397,10 @@ const envSchema = z
(data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE) || data.NODE_ENV === "test", (data.NODE_ENV === "development" && data.ROTATION_DEVELOPMENT_MODE) || data.NODE_ENV === "test",
isDailyResourceCleanUpDevelopmentMode: isDailyResourceCleanUpDevelopmentMode:
data.NODE_ENV === "development" && data.DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE, data.NODE_ENV === "development" && data.DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE,
isAcmeFeatureEnabled: data.NODE_ENV === "development" && data.ACME_FEATURE_ENABLED === true,
isAcmeDevelopmentMode: data.NODE_ENV === "development" && data.ACME_DEVELOPMENT_MODE, isAcmeDevelopmentMode: data.NODE_ENV === "development" && data.ACME_DEVELOPMENT_MODE,
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED, isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS), isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS),
isBddNockApiEnabled: data.NODE_ENV === "development" && data.BDD_NOCK_API_ENABLED,
REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim() REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim()
?.split(",") ?.split(",")
.map((el) => { .map((el) => {
+1 -2
View File
@@ -252,8 +252,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
error: error.name, error: error.name,
status: error.status, status: error.status,
type: `urn:ietf:params:acme:error:${error.type}`, type: `urn:ietf:params:acme:error:${error.type}`,
detail: error.detail, detail: error.message
message: error.message
// TODO: add subproblems if they exist // TODO: add subproblems if they exist
}); });
} else { } else {
+1 -4
View File
@@ -31,10 +31,7 @@ export const registerServeUI = async (
CAPTCHA_SITE_KEY: appCfg.CAPTCHA_SITE_KEY, CAPTCHA_SITE_KEY: appCfg.CAPTCHA_SITE_KEY,
POSTHOG_API_KEY: appCfg.POSTHOG_PROJECT_API_KEY, POSTHOG_API_KEY: appCfg.POSTHOG_PROJECT_API_KEY,
INTERCOM_ID: appCfg.INTERCOM_ID, INTERCOM_ID: appCfg.INTERCOM_ID,
TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED, TELEMETRY_CAPTURING_ENABLED: appCfg.TELEMETRY_ENABLED
// The feature flag to enable/disable the ACME feature.
// Will be removed once the feature is ready for production.
ACME_FEATURE_ENABLED: appCfg.isAcmeFeatureEnabled
}; };
const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`; const js = `window.__INFISICAL_RUNTIME_ENV__ = Object.freeze(${JSON.stringify(config)});`;
return res.send(js); return res.send(js);
+25 -2
View File
@@ -241,6 +241,8 @@ import { identityTokenAuthServiceFactory } from "@app/services/identity-token-au
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal"; import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal"; import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal";
import { identityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { identityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { identityV2DALFactory } from "@app/services/identity-v2/identity-dal";
import { identityV2ServiceFactory } from "@app/services/identity-v2/identity-service";
import { integrationDALFactory } from "@app/services/integration/integration-dal"; import { integrationDALFactory } from "@app/services/integration/integration-dal";
import { integrationServiceFactory } from "@app/services/integration/integration-service"; import { integrationServiceFactory } from "@app/services/integration/integration-service";
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal"; import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
@@ -445,6 +447,7 @@ export const registerRoutes = async (
const serviceTokenDAL = serviceTokenDALFactory(db); const serviceTokenDAL = serviceTokenDALFactory(db);
const identityDAL = identityDALFactory(db); const identityDAL = identityDALFactory(db);
const identityV2DAL = identityV2DALFactory(db);
const identityMetadataDAL = identityMetadataDALFactory(db); const identityMetadataDAL = identityMetadataDALFactory(db);
const identityAccessTokenDAL = identityAccessTokenDALFactory(db); const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
const identityOrgMembershipDAL = identityOrgDALFactory(db); const identityOrgMembershipDAL = identityOrgDALFactory(db);
@@ -640,7 +643,8 @@ export const registerRoutes = async (
projectDAL, projectDAL,
identityDAL, identityDAL,
userDAL, userDAL,
externalGroupOrgRoleMappingDAL externalGroupOrgRoleMappingDAL,
membershipRoleDAL
}); });
const additionalPrivilegeService = additionalPrivilegeServiceFactory({ const additionalPrivilegeService = additionalPrivilegeServiceFactory({
additionalPrivilegeDAL, additionalPrivilegeDAL,
@@ -1184,6 +1188,7 @@ export const registerRoutes = async (
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
permissionService, permissionService,
licenseService,
kmsService, kmsService,
projectDAL projectDAL
}); });
@@ -1655,6 +1660,17 @@ export const registerRoutes = async (
membershipIdentityDAL, membershipIdentityDAL,
membershipRoleDAL membershipRoleDAL
}); });
const identityV2Service = identityV2ServiceFactory({
membershipIdentityDAL,
membershipRoleDAL,
identityMetadataDAL,
licenseService,
permissionService,
identityDAL: identityV2DAL,
keyStore
});
const identityProjectService = identityProjectServiceFactory({ const identityProjectService = identityProjectServiceFactory({
identityProjectDAL, identityProjectDAL,
membershipIdentityDAL, membershipIdentityDAL,
@@ -2229,8 +2245,13 @@ export const registerRoutes = async (
}); });
const pkiAcmeService = pkiAcmeServiceFactory({ const pkiAcmeService = pkiAcmeServiceFactory({
projectDAL, projectDAL,
appConnectionDAL,
certificateDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateProfileDAL, certificateProfileDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL,
acmeAccountDAL, acmeAccountDAL,
acmeOrderDAL, acmeOrderDAL,
acmeAuthDAL, acmeAuthDAL,
@@ -2238,6 +2259,7 @@ export const registerRoutes = async (
acmeChallengeDAL, acmeChallengeDAL,
keyStore, keyStore,
kmsService, kmsService,
licenseService,
certificateV3Service, certificateV3Service,
acmeChallengeService acmeChallengeService
}); });
@@ -2457,7 +2479,8 @@ export const registerRoutes = async (
integrationAuth: integrationAuthService, integrationAuth: integrationAuthService,
webhook: webhookService, webhook: webhookService,
serviceToken: serviceTokenService, serviceToken: serviceTokenService,
identity: identityService, identityV1: identityService,
identityV2: identityV2Service,
identityAuthTemplate: identityAuthTemplateService, identityAuthTemplate: identityAuthTemplateService,
identityAccessToken: identityAccessTokenService, identityAccessToken: identityAccessTokenService,
identityTokenAuth: identityTokenAuthService, identityTokenAuth: identityTokenAuthService,
@@ -0,0 +1,87 @@
// import { z } from "zod";
// import { getConfig } from "@app/lib/config/env";
// import { ForbiddenRequestError } from "@app/lib/errors";
// import { logger } from "@app/lib/logger";
// import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
// import { AuthMode } from "@app/services/auth/auth-type";
// export const registerBddNockRouter = async (server: FastifyZodProvider) => {
// const checkIfBddNockApiEnabled = () => {
// const appCfg = getConfig();
// // Note: Please note that this API is only available in development mode and only for BDD tests.
// // This endpoint should NEVER BE ENABLED IN PRODUCTION!
// if (appCfg.NODE_ENV !== "development" || !appCfg.isBddNockApiEnabled) {
// throw new ForbiddenRequestError({ message: "BDD Nock API is not enabled" });
// }
// };
// server.route({
// method: "POST",
// url: "/define",
// schema: {
// body: z.object({ definitions: z.unknown().array() }),
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async (req) => {
// checkIfBddNockApiEnabled();
// const { body } = req;
// const { definitions } = body;
// logger.info(definitions, "Defining nock");
// const processedDefinitions = definitions.map((definition: unknown) => {
// const { path, ...rest } = definition as Definition;
// return {
// ...rest,
// path:
// path !== undefined && typeof path === "string"
// ? path
// : new RegExp((path as unknown as { regex: string }).regex ?? "")
// } as Definition;
// });
// nock.define(processedDefinitions);
// // Ensure we are activating the nocks, because we could have called `nock.restore()` before this call.
// if (!nock.isActive()) {
// nock.activate();
// }
// return { status: "ok" };
// }
// });
// server.route({
// method: "POST",
// url: "/clean-all",
// schema: {
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async () => {
// checkIfBddNockApiEnabled();
// logger.info("Cleaning all nocks");
// nock.cleanAll();
// return { status: "ok" };
// }
// });
// server.route({
// method: "POST",
// url: "/restore",
// schema: {
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async () => {
// checkIfBddNockApiEnabled();
// logger.info("Restore network requests from nock");
// nock.restore();
// return { status: "ok" };
// }
// });
// };
@@ -19,7 +19,7 @@ import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-
import { SanitizedProjectSchema } from "../sanitizedSchemas"; import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerIdentityProjectRouter = async (server: FastifyZodProvider) => { export const registerDeprecatedIdentityProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/:projectId/identity-memberships/:identityId", url: "/:projectId/identity-memberships/:identityId",
@@ -293,7 +293,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
temporaryAccessEndTime: z.date().nullable().optional() temporaryAccessEndTime: z.date().nullable().optional()
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, projectId: true, orgId: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}), }),
project: SanitizedProjectSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
@@ -362,7 +362,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
temporaryAccessEndTime: z.date().nullable().optional() temporaryAccessEndTime: z.date().nullable().optional()
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ lastLoginAuthMethod: z.string().nullable().optional(),
lastLoginTime: z.date().nullable().optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, projectId: true, orgId: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}), }),
project: SanitizedProjectSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
@@ -1,9 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; import { AccessScope, IdentitiesSchema, MembershipRolesSchema, TemporaryPermissionMode } from "@app/db/schemas";
import { ApiDocsTags, PROJECT_IDENTITIES } from "@app/lib/api-docs"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, ORG_IDENTITY_MEMBERSHIP } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -15,7 +16,7 @@ const sanitizedOrgIdentityMembershipSchema = z.object({
updatedAt: z.date() updatedAt: z.date()
}); });
export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProvider) => { export const registerIdentityOrgMembershipRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/identity-memberships/:identityId", url: "/identity-memberships/:identityId",
@@ -25,8 +26,7 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
hide: true, hide: true,
// this is hidden so not updating tags tags: [ApiDocsTags.OrgIdentityMembership],
tags: [ApiDocsTags.ProjectIdentities],
description: "Create org identity membership", description: "Create org identity membership",
security: [ security: [
{ {
@@ -34,38 +34,40 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
} }
], ],
params: z.object({ params: z.object({
identityId: z.string().trim() identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.identityId)
}), }),
body: z.object({ body: z.object({
roles: z roles: z
.array( .array(
z.union([ z.union([
z.object({ z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z isTemporary: z
.literal(false) .literal(false)
.default(false) .default(false)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role) .describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}), }),
z.object({ z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), isTemporary: z
.literal(true)
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z temporaryMode: z
.nativeEnum(TemporaryPermissionMode) .nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), .describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z temporaryRange: z
.string() .string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number") .refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), .describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z temporaryAccessStartTime: z
.string() .string()
.datetime() .datetime()
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role) .describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
}) })
]) ])
) )
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description) .describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.max(1) .min(1)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -86,12 +88,115 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
} }
}); });
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_IDENTITY_ORG_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return { return {
identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId } identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId }
}; };
} }
}); });
server.route({
method: "PATCH",
url: "/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "Update org identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(true)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.min(1)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.description)
}),
response: {
200: z.object({
roles: MembershipRolesSchema.array()
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.updateMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
},
data: {
roles: req.body.roles
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_IDENTITY_ORG_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
roles: membership.roles.map((el) => ({ ...el, membershipId: membership.id }))
};
}
});
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/identity-memberships/:identityId", url: "/identity-memberships/:identityId",
@@ -101,15 +206,15 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
hide: true, hide: true,
tags: [ApiDocsTags.ProjectIdentities], tags: [ApiDocsTags.OrgIdentityMembership],
description: "Delete org identity memberships", description: "Delete org identity membership",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
} }
], ],
params: z.object({ params: z.object({
identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId) identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.DELETE_IDENTITY_MEMBERSHIP.identityId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -129,9 +234,226 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
} }
}); });
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_IDENTITY_ORG_MEMBERSHIP,
metadata: {
identityId: req.params.identityId
}
}
});
return { return {
identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId } identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId }
}; };
} }
}); });
server.route({
method: "GET",
url: "/identity-memberships",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "List org identity memberships",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(100)
.default(20)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.limit)
.optional(),
identityName: z
.string()
.trim()
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.identityName)
.optional(),
roles: z
.string()
.transform((val) => val.split(",").map((role) => role.trim()))
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.roles)
.optional()
}),
response: {
200: z.object({
identityMemberships: z
.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true })
})
.array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { data: identityMemberships, totalCount } = await server.services.membershipIdentity.listMemberships({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName,
roles: req.query.roles
}
});
return { identityMemberships, totalCount };
}
});
server.route({
method: "GET",
url: "/identity-memberships/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "Get org identity membership by identity ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.GET_IDENTITY_MEMBERSHIP_BY_ID.identityId)
}),
response: {
200: z.object({
identityMembership: z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true }).extend({
authMethods: z.array(z.string())
})
})
})
}
},
handler: async (req) => {
const identityMembership = await server.services.membershipIdentity.getMembershipByIdentityId({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
}
});
return { identityMembership };
}
});
server.route({
method: "GET",
url: "/available-identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: false,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "List available identities for org membership",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(100)
.default(20)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.limit)
.optional(),
identityName: z.string().describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.identityName).optional()
}),
response: {
200: z.object({
identities: IdentitiesSchema.pick({ id: true, name: true }).array()
})
}
},
handler: async (req) => {
const { identities } = await server.services.membershipIdentity.listAvailableIdentities({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName
}
});
return { identities };
}
});
}; };
@@ -0,0 +1,493 @@
import { z } from "zod";
import {
AccessScope,
IdentitiesSchema,
IdentityProjectMembershipsSchema,
ProjectMembershipRole,
TemporaryPermissionMode
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECT_IDENTITIES, PROJECT_IDENTITY_MEMBERSHIP } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
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";
export const registerIdentityProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Create project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim(),
identityId: z.string().trim()
}),
body: z.object({
// @depreciated
role: z.string().trim().optional().default(ProjectMembershipRole.NoAccess),
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
})
])
)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.optional()
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const { role, roles } = req.body;
if (!role && !roles) throw new BadRequestError({ message: "You must provide either role or roles field" });
const { membership } = await server.services.membershipIdentity.createMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
identityId: req.params.identityId,
roles: roles || [{ role, isTemporary: false }]
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.projectId,
event: {
type: EventType.CREATE_IDENTITY_PROJECT_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "PATCH",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Update project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.min(1)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.description)
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.updateMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
},
data: {
roles: req.body.roles
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.projectId,
event: {
type: EventType.UPDATE_IDENTITY_PROJECT_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "DELETE",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Delete project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId)
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.deleteMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.projectId,
event: {
type: EventType.DELETE_IDENTITY_PROJECT_MEMBERSHIP,
metadata: {
identityId: req.params.identityId
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "GET",
url: "/identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.IdentityProjectMembership],
description: "List project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.projectId)
}),
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(1000)
.default(20)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.limit)
.optional(),
identityName: z
.string()
.trim()
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.identityName)
.optional(),
roles: z
.string()
.transform((val) => val.split(",").map((role) => role.trim()))
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.roles)
.optional()
}),
response: {
200: z.object({
identityMemberships: z
.object({
id: z.string(),
identityId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true })
})
.array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { data: identityMemberships, totalCount } = await server.services.membershipIdentity.listMemberships({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName,
roles: req.query.roles
}
});
return { identityMemberships, totalCount };
}
});
server.route({
method: "GET",
url: "/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.IdentityProjectMembership],
description: "Get project identity membership by identity ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.GET_IDENTITY_MEMBERSHIP_BY_ID.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.GET_IDENTITY_MEMBERSHIP_BY_ID.identityId)
}),
response: {
200: z.object({
identityMembership: z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
lastLoginAuthMethod: z.string().nullable().optional(),
lastLoginTime: z.date().nullable().optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true }).extend({
authMethods: z.array(z.string()),
metadata: z
.object({
id: z.string().trim().min(1),
key: z.string().trim().min(1),
value: z.string().trim().min(1)
})
.array()
.optional()
})
})
})
}
},
handler: async (req) => {
const identityMembership = await server.services.membershipIdentity.getMembershipByIdentityId({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
return { identityMembership };
}
});
server.route({
method: "GET",
url: "/available-identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: false,
tags: [ApiDocsTags.IdentityProjectMembership],
description: "List available identities for project membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.projectId)
}),
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(1000)
.default(20)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.limit)
.optional(),
identityName: z
.string()
.trim()
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.identityName)
.optional()
}),
response: {
200: z.object({
identities: IdentitiesSchema.pick({ id: true, name: true }).array()
})
}
},
handler: async (req) => {
const { identities } = await server.services.membershipIdentity.listAvailableIdentities({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName
}
});
return { identities };
}
});
};
@@ -60,7 +60,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const identity = await server.services.identity.createIdentity({ const identity = await server.services.identityV1.createIdentity({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -136,7 +136,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const identity = await server.services.identity.updateIdentity({ const identity = await server.services.identityV1.updateIdentity({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -189,7 +189,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const identity = await server.services.identity.deleteIdentity({ const identity = await server.services.identityV1.deleteIdentity({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -258,7 +258,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const identity = await server.services.identity.getIdentityById({ const identity = await server.services.identityV1.getIdentityById({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -308,7 +308,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({ const { identityMemberships, totalCount } = await server.services.identityV1.listOrgIdentities({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -402,7 +402,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.searchOrgIdentities({ const { identityMemberships, totalCount } = await server.services.identityV1.searchOrgIdentities({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -468,7 +468,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const identityMemberships = await server.services.identity.listProjectIdentitiesByIdentityId({ const identityMemberships = await server.services.identityV1.listProjectIdentitiesByIdentityId({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
+26 -5
View File
@@ -8,12 +8,14 @@ import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/
import { registerAdminRouter } from "./admin-router"; import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router"; import { registerAuthRoutes } from "./auth-router";
// import { registerBddNockRouter } from "./bdd-nock-router";
import { registerProjectBotRouter } from "./bot-router"; import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router"; import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers"; import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
import { registerCertificateProfilesRouter } from "./certificate-profiles-router"; import { registerCertificateProfilesRouter } from "./certificate-profiles-router";
import { registerCertRouter } from "./certificate-router"; import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerDeprecatedIdentityProjectMembershipRouter } from "./deprecated-identity-project-membership-router";
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router"; import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router"; import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
import { registerDeprecatedProjectRouter } from "./deprecated-project-router"; import { registerDeprecatedProjectRouter } from "./deprecated-project-router";
@@ -33,8 +35,8 @@ import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-rou
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router"; import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router"; import { registerIdentityOrgMembershipRouter } from "./identity-org-membership-router";
import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerIdentityProjectMembershipRouter } from "./identity-project-membership-router";
import { registerIdentityRouter } from "./identity-router"; import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router"; import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
@@ -45,6 +47,7 @@ import { registerInviteOrgRouter } from "./invite-org-router";
import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router"; import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router";
import { registerNotificationRouter } from "./notification-router"; import { registerNotificationRouter } from "./notification-router";
import { registerOrgAdminRouter } from "./org-admin-router"; import { registerOrgAdminRouter } from "./org-admin-router";
import { registerOrgIdentityRouter } from "./org-identity-router";
import { registerOrgRouter } from "./organization-router"; import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router"; import { registerPasswordRouter } from "./password-router";
import { registerPkiAlertRouter } from "./pki-alert-router"; import { registerPkiAlertRouter } from "./pki-alert-router";
@@ -52,6 +55,7 @@ import { registerPkiCollectionRouter } from "./pki-collection-router";
import { registerPkiSubscriberRouter } from "./pki-subscriber-router"; import { registerPkiSubscriberRouter } from "./pki-subscriber-router";
import { PKI_SYNC_REGISTER_ROUTER_MAP, registerPkiSyncRouter } from "./pki-sync-routers"; import { PKI_SYNC_REGISTER_ROUTER_MAP, registerPkiSyncRouter } from "./pki-sync-routers";
import { registerProjectEnvRouter } from "./project-env-router"; import { registerProjectEnvRouter } from "./project-env-router";
import { registerProjectIdentityRouter } from "./project-identity-router";
import { registerProjectKeyRouter } from "./project-key-router"; import { registerProjectKeyRouter } from "./project-key-router";
import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router"; import { registerProjectRouter } from "./project-router";
@@ -90,8 +94,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
{ prefix: "/auth" } { prefix: "/auth" }
); );
await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerOrgRouter, { prefix: "/organization" }); await server.register(
await server.register(registerOrgIdentityMembershipRouter, { prefix: "/organization" }); async (orgRouter) => {
await orgRouter.register(registerOrgRouter);
await orgRouter.register(registerOrgIdentityRouter);
await orgRouter.register(registerIdentityOrgMembershipRouter);
},
{ prefix: "/organization" }
);
await server.register(registerAdminRouter, { prefix: "/admin" }); await server.register(registerAdminRouter, { prefix: "/admin" });
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" }); await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
await server.register(registerUserRouter, { prefix: "/user" }); await server.register(registerUserRouter, { prefix: "/user" });
@@ -126,14 +136,19 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
async (projectRouter) => { async (projectRouter) => {
await projectRouter.register(registerProjectRouter); await projectRouter.register(registerProjectRouter);
await projectRouter.register(registerProjectMembershipRouter); await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerProjectIdentityRouter);
await projectRouter.register(registerProjectEnvRouter); await projectRouter.register(registerProjectEnvRouter);
await projectRouter.register(registerSecretTagRouter); await projectRouter.register(registerSecretTagRouter);
await projectRouter.register(registerGroupProjectRouter); await projectRouter.register(registerGroupProjectRouter);
await projectRouter.register(registerIdentityProjectRouter); await projectRouter.register(registerDeprecatedIdentityProjectMembershipRouter);
}, },
{ prefix: "/projects" } { prefix: "/projects" }
); );
await server.register(registerIdentityProjectMembershipRouter, {
prefix: "/projects/:projectId/memberships"
});
await server.register( await server.register(
async (pkiRouter) => { async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
@@ -223,4 +238,10 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerEventRouter, { prefix: "/events" }); await server.register(registerEventRouter, { prefix: "/events" });
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" }); await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
// Note: This is a special route for BDD tests. It's only available in development mode and only for BDD tests.
// This route should NEVER BE ENABLED IN PRODUCTION!
// if (getConfig().isBddNockApiEnabled) {
// await server.register(registerBddNockRouter, { prefix: "/bdd-nock" });
// }
}; };
@@ -0,0 +1,286 @@
import { z } from "zod";
import { AccessScope, IdentitiesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, IDENTITIES } 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";
const metadataSchema = z.object({
key: z.string().trim().min(1, "Metadata key cannot be empty"),
value: z.string().trim().min(1, "Metadata value cannot be empty")
});
const sanitizedIdentitySchema = IdentitiesSchema.pick({
id: true,
name: true,
orgId: true,
projectId: true,
createdAt: true,
updatedAt: true,
hasDeleteProtection: true
}).extend({
authMethods: z.array(z.string()).optional(),
metadata: z.array(metadataSchema).optional()
});
export const registerOrgIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/identities",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Create an identity",
security: [
{
bearerAuth: []
}
],
body: z.object({
name: z.string().trim().min(1).describe(IDENTITIES.CREATE.name),
hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.CREATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.createIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_IDENTITY,
metadata: {
identityId: identity.id,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "PATCH",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Update an identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(IDENTITIES.UPDATE.identityId)
}),
body: z.object({
name: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.name),
hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.UPDATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.updateIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_IDENTITY,
metadata: {
identityId: req.params.identityId,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "DELETE",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Delete an identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(IDENTITIES.DELETE.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.deleteIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_IDENTITY,
metadata: {
identityId: req.params.identityId
}
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Get an identity by ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(IDENTITIES.GET_BY_ID.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.getIdentityById({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "List identities",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
offset: z.coerce.number().min(0).default(0).describe(IDENTITIES.LIST.offset).optional(),
limit: z.coerce.number().min(1).max(1000).default(20).describe(IDENTITIES.LIST.limit).optional(),
search: z.string().trim().describe(IDENTITIES.LIST.search).optional()
}),
response: {
200: z.object({
identities: z.array(sanitizedIdentitySchema),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { docs: identities, count: totalCount } = await server.services.identityV2.listIdentities({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
search: req.query.search
}
});
return { identities, totalCount };
}
});
};
@@ -0,0 +1,313 @@
import { z } from "zod";
import { AccessScope, IdentitiesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, IDENTITIES } 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";
const metadataSchema = z.object({
key: z.string().trim().min(1, "Metadata key cannot be empty"),
value: z.string().trim().min(1, "Metadata value cannot be empty")
});
const sanitizedIdentitySchema = IdentitiesSchema.pick({
id: true,
name: true,
orgId: true,
projectId: true,
createdAt: true,
updatedAt: true,
hasDeleteProtection: true
}).extend({
activeLockoutAuthMethods: z.string().array().optional(),
authMethods: z.string().array().optional(),
metadata: z
.object({
key: z.string(),
value: z.string(),
id: z.string()
})
.array()
.optional()
});
export const registerProjectIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/identities",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Create an identity in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project to create the identity in")
}),
body: z.object({
name: z.string().trim().min(1).describe(IDENTITIES.CREATE.name),
hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.CREATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.createIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_IDENTITY,
metadata: {
identityId: identity.id,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "PATCH",
url: "/:projectId/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Update an identity in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project"),
identityId: z.string().trim().describe(IDENTITIES.UPDATE.identityId)
}),
body: z.object({
name: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.name),
hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.UPDATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.updateIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
projectId: req.params.projectId,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_IDENTITY,
metadata: {
identityId: req.params.identityId,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "DELETE",
url: "/:projectId/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Delete an identity from a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project"),
identityId: z.string().trim().describe(IDENTITIES.DELETE.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.deleteIdentity({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Project,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_IDENTITY,
metadata: {
identityId: req.params.identityId
}
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/:projectId/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Get an identity by ID in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project"),
identityId: z.string().trim().describe(IDENTITIES.GET_BY_ID.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.getIdentityById({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Project,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/:projectId/identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "List identities in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project")
}),
querystring: z.object({
offset: z.coerce.number().min(0).default(0).describe(IDENTITIES.LIST.offset).optional(),
limit: z.coerce.number().min(1).max(1000).default(20).describe(IDENTITIES.LIST.limit).optional(),
search: z.string().trim().describe(IDENTITIES.LIST.search).optional()
}),
response: {
200: z.object({
identities: z.array(sanitizedIdentitySchema),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { docs: identities, count: totalCount } = await server.services.identityV2.listIdentities({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Project,
projectId: req.params.projectId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
search: req.query.search
}
});
return { identities, totalCount };
}
});
};
@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { AccessScope, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas"; import { AccessScope, OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs"; import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
@@ -51,6 +51,19 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const usernamesAndEmails = [...req.body.emails, ...req.body.usernames]; const usernamesAndEmails = [...req.body.emails, ...req.body.usernames];
await server.services.membershipUser.createMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
roles: [{ isTemporary: false, role: OrgMembershipRole.NoAccess }],
usernames: usernamesAndEmails
}
});
const { memberships } = await server.services.membershipUser.createMembership({ const { memberships } = await server.services.membershipUser.createMembership({
permission: req.permission, permission: req.permission,
scopeData: { scopeData: {
@@ -70,7 +70,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({ const { identityMemberships, totalCount } = await server.services.identityV1.listOrgIdentities({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { OnePassConnectionMethod } from "./1password-connection-enums"; import { OnePassConnectionMethod } from "./1password-connection-enums";
export const OnePassConnectionAccessTokenCredentialsSchema = z.object({ export const OnePassConnectionAccessTokenCredentialsSchema = z.object({
@@ -33,7 +34,7 @@ export const SanitizedOnePassConnectionSchema = z.discriminatedUnion("method", [
credentials: OnePassConnectionAccessTokenCredentialsSchema.pick({ credentials: OnePassConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true instanceUrl: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.OnePass]} (API Token)` }))
]); ]);
export const ValidateOnePassConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateOnePassConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -57,8 +58,10 @@ export const UpdateOnePassConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OnePass)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OnePass));
export const OnePassConnectionListItemSchema = z.object({ export const OnePassConnectionListItemSchema = z
name: z.literal("1Password"), .object({
app: z.literal(AppConnection.OnePass), name: z.literal("1Password"),
methods: z.nativeEnum(OnePassConnectionMethod).array() app: z.literal(AppConnection.OnePass),
}); methods: z.nativeEnum(OnePassConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.OnePass] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { Auth0ConnectionMethod } from "./auth0-connection-enums"; import { Auth0ConnectionMethod } from "./auth0-connection-enums";
export const Auth0ConnectionClientCredentialsInputCredentialsSchema = z.object({ export const Auth0ConnectionClientCredentialsInputCredentialsSchema = z.object({
@@ -59,7 +60,7 @@ export const SanitizedAuth0ConnectionSchema = z.discriminatedUnion("method", [
clientId: true, clientId: true,
audience: true audience: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Auth0]} (Client Credentials)` }))
]); ]);
export const ValidateAuth0ConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateAuth0ConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -85,10 +86,12 @@ export const UpdateAuth0ConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Auth0)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Auth0));
export const Auth0ConnectionListItemSchema = z.object({ export const Auth0ConnectionListItemSchema = z
name: z.literal("Auth0"), .object({
app: z.literal(AppConnection.Auth0), name: z.literal("Auth0"),
// the below is preferable but currently breaks with our zod to json schema parser app: z.literal(AppConnection.Auth0),
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]), // the below is preferable but currently breaks with our zod to json schema parser
methods: z.nativeEnum(Auth0ConnectionMethod).array() // methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
}); methods: z.nativeEnum(Auth0ConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Auth0] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AwsConnectionMethod } from "./aws-connection-enums"; import { AwsConnectionMethod } from "./aws-connection-enums";
export const AwsConnectionAssumeRoleCredentialsSchema = z.object({ export const AwsConnectionAssumeRoleCredentialsSchema = z.object({
@@ -39,11 +40,11 @@ export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
BaseAwsConnectionSchema.extend({ BaseAwsConnectionSchema.extend({
method: z.literal(AwsConnectionMethod.AssumeRole), method: z.literal(AwsConnectionMethod.AssumeRole),
credentials: AwsConnectionAssumeRoleCredentialsSchema.pick({}) credentials: AwsConnectionAssumeRoleCredentialsSchema.pick({})
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AWS]} (Assume Role)` })),
BaseAwsConnectionSchema.extend({ BaseAwsConnectionSchema.extend({
method: z.literal(AwsConnectionMethod.AccessKey), method: z.literal(AwsConnectionMethod.AccessKey),
credentials: AwsConnectionAccessTokenCredentialsSchema.pick({ accessKeyId: true }) credentials: AwsConnectionAccessTokenCredentialsSchema.pick({ accessKeyId: true })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AWS]} (Access Key)` }))
]); ]);
export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -72,11 +73,13 @@ export const UpdateAwsConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AWS)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AWS));
export const AwsConnectionListItemSchema = z.object({ export const AwsConnectionListItemSchema = z
name: z.literal("AWS"), .object({
app: z.literal(AppConnection.AWS), name: z.literal("AWS"),
// the below is preferable but currently breaks with our zod to json schema parser app: z.literal(AppConnection.AWS),
// methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]), // the below is preferable but currently breaks with our zod to json schema parser
methods: z.nativeEnum(AwsConnectionMethod).array(), // methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]),
accessKeyId: z.string().optional() methods: z.nativeEnum(AwsConnectionMethod).array(),
}); accessKeyId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AWS] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums"; import { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums";
export const AzureADCSUsernamePasswordCredentialsSchema = z.object({ export const AzureADCSUsernamePasswordCredentialsSchema = z.object({
@@ -55,7 +56,7 @@ export const SanitizedAzureADCSConnectionSchema = z.discriminatedUnion("method",
sslRejectUnauthorized: true, sslRejectUnauthorized: true,
sslCertificate: true sslCertificate: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureADCS]} (Username and Password)` }))
]); ]);
export const ValidateAzureADCSConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateAzureADCSConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -81,8 +82,10 @@ export const UpdateAzureADCSConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureADCS)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureADCS));
export const AzureADCSConnectionListItemSchema = z.object({ export const AzureADCSConnectionListItemSchema = z
name: z.literal("Azure ADCS"), .object({
app: z.literal(AppConnection.AzureADCS), name: z.literal("Azure ADCS"),
methods: z.nativeEnum(AzureADCSConnectionMethod).array() app: z.literal(AppConnection.AzureADCS),
}); methods: z.nativeEnum(AzureADCSConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AzureADCS] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums"; import { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums";
export const AzureAppConfigurationConnectionOAuthInputCredentialsSchema = z.object({ export const AzureAppConfigurationConnectionOAuthInputCredentialsSchema = z.object({
@@ -104,19 +105,23 @@ export const SanitizedAzureAppConfigurationConnectionSchema = z.discriminatedUni
credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema.pick({ credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true tenantId: true
}) })
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureAppConfiguration]} (OAuth)` })),
BaseAzureAppConfigurationConnectionSchema.extend({ BaseAzureAppConfigurationConnectionSchema.extend({
method: z.literal(AzureAppConfigurationConnectionMethod.ClientSecret), method: z.literal(AzureAppConfigurationConnectionMethod.ClientSecret),
credentials: AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema.pick({ credentials: AzureAppConfigurationConnectionClientSecretOutputCredentialsSchema.pick({
clientId: true, clientId: true,
tenantId: true tenantId: true
}) })
}) }).describe(
JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureAppConfiguration]} (Client Secret)` })
)
]); ]);
export const AzureAppConfigurationConnectionListItemSchema = z.object({ export const AzureAppConfigurationConnectionListItemSchema = z
name: z.literal("Azure App Configuration"), .object({
app: z.literal(AppConnection.AzureAppConfiguration), name: z.literal("Azure App Configuration"),
methods: z.nativeEnum(AzureAppConfigurationConnectionMethod).array(), app: z.literal(AppConnection.AzureAppConfiguration),
oauthClientId: z.string().optional() methods: z.nativeEnum(AzureAppConfigurationConnectionMethod).array(),
}); oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AzureAppConfiguration] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums"; import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
export const AzureClientSecretsConnectionOAuthInputCredentialsSchema = z.object({ export const AzureClientSecretsConnectionOAuthInputCredentialsSchema = z.object({
@@ -162,26 +163,30 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion(
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({ credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true tenantId: true
}) })
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureClientSecrets]} (OAuth)` })),
BaseAzureClientSecretsConnectionSchema.extend({ BaseAzureClientSecretsConnectionSchema.extend({
method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret), method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema.pick({ credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema.pick({
clientId: true, clientId: true,
tenantId: true tenantId: true
}) })
}), }).describe(
JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureClientSecrets]} (Client Secret)` })
),
BaseAzureClientSecretsConnectionSchema.extend({ BaseAzureClientSecretsConnectionSchema.extend({
method: z.literal(AzureClientSecretsConnectionMethod.Certificate), method: z.literal(AzureClientSecretsConnectionMethod.Certificate),
credentials: AzureClientSecretsConnectionCertificateOutputCredentialsSchema.pick({ credentials: AzureClientSecretsConnectionCertificateOutputCredentialsSchema.pick({
tenantId: true, tenantId: true,
clientId: true clientId: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureClientSecrets]} (Certificate)` }))
]); ]);
export const AzureClientSecretsConnectionListItemSchema = z.object({ export const AzureClientSecretsConnectionListItemSchema = z
name: z.literal("Azure Client Secrets"), .object({
app: z.literal(AppConnection.AzureClientSecrets), name: z.literal("Azure Client Secrets"),
methods: z.nativeEnum(AzureClientSecretsConnectionMethod).array(), app: z.literal(AppConnection.AzureClientSecrets),
oauthClientId: z.string().optional() methods: z.nativeEnum(AzureClientSecretsConnectionMethod).array(),
}); oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AzureClientSecrets] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AzureDevOpsConnectionMethod } from "./azure-devops-enums"; import { AzureDevOpsConnectionMethod } from "./azure-devops-enums";
export const AzureDevOpsConnectionOAuthInputCredentialsSchema = z.object({ export const AzureDevOpsConnectionOAuthInputCredentialsSchema = z.object({
@@ -147,13 +148,13 @@ export const SanitizedAzureDevOpsConnectionSchema = z.discriminatedUnion("method
tenantId: true, tenantId: true,
orgName: true orgName: true
}) })
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureDevOps]} (OAuth)` })),
BaseAzureDevOpsConnectionSchema.extend({ BaseAzureDevOpsConnectionSchema.extend({
method: z.literal(AzureDevOpsConnectionMethod.AccessToken), method: z.literal(AzureDevOpsConnectionMethod.AccessToken),
credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema.pick({ credentials: AzureDevOpsConnectionAccessTokenOutputCredentialsSchema.pick({
orgName: true orgName: true
}) })
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureDevOps]} (Access Token)` })),
BaseAzureDevOpsConnectionSchema.extend({ BaseAzureDevOpsConnectionSchema.extend({
method: z.literal(AzureDevOpsConnectionMethod.ClientSecret), method: z.literal(AzureDevOpsConnectionMethod.ClientSecret),
credentials: AzureDevOpsConnectionClientSecretOutputCredentialsSchema.pick({ credentials: AzureDevOpsConnectionClientSecretOutputCredentialsSchema.pick({
@@ -161,12 +162,14 @@ export const SanitizedAzureDevOpsConnectionSchema = z.discriminatedUnion("method
tenantId: true, tenantId: true,
orgName: true orgName: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureDevOps]} (Client Secret)` }))
]); ]);
export const AzureDevOpsConnectionListItemSchema = z.object({ export const AzureDevOpsConnectionListItemSchema = z
name: z.literal("Azure DevOps"), .object({
app: z.literal(AppConnection.AzureDevOps), name: z.literal("Azure DevOps"),
methods: z.nativeEnum(AzureDevOpsConnectionMethod).array(), app: z.literal(AppConnection.AzureDevOps),
oauthClientId: z.string().optional() methods: z.nativeEnum(AzureDevOpsConnectionMethod).array(),
}); oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AzureDevOps] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums"; import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums";
export const AzureKeyVaultConnectionOAuthInputCredentialsSchema = z.object({ export const AzureKeyVaultConnectionOAuthInputCredentialsSchema = z.object({
@@ -104,19 +105,21 @@ export const SanitizedAzureKeyVaultConnectionSchema = z.discriminatedUnion("meth
credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema.pick({ credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true tenantId: true
}) })
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureKeyVault]} (OAuth)` })),
BaseAzureKeyVaultConnectionSchema.extend({ BaseAzureKeyVaultConnectionSchema.extend({
method: z.literal(AzureKeyVaultConnectionMethod.ClientSecret), method: z.literal(AzureKeyVaultConnectionMethod.ClientSecret),
credentials: AzureKeyVaultConnectionClientSecretOutputCredentialsSchema.pick({ credentials: AzureKeyVaultConnectionClientSecretOutputCredentialsSchema.pick({
clientId: true, clientId: true,
tenantId: true tenantId: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.AzureKeyVault]} (Client Secret)` }))
]); ]);
export const AzureKeyVaultConnectionListItemSchema = z.object({ export const AzureKeyVaultConnectionListItemSchema = z
name: z.literal("Azure Key Vault"), .object({
app: z.literal(AppConnection.AzureKeyVault), name: z.literal("Azure Key Vault"),
methods: z.nativeEnum(AzureKeyVaultConnectionMethod).array(), app: z.literal(AppConnection.AzureKeyVault),
oauthClientId: z.string().optional() methods: z.nativeEnum(AzureKeyVaultConnectionMethod).array(),
}); oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.AzureKeyVault] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { BitbucketConnectionMethod } from "./bitbucket-connection-enums"; import { BitbucketConnectionMethod } from "./bitbucket-connection-enums";
export const BitbucketConnectionAccessTokenCredentialsSchema = z.object({ export const BitbucketConnectionAccessTokenCredentialsSchema = z.object({
@@ -39,7 +40,7 @@ export const SanitizedBitbucketConnectionSchema = z.discriminatedUnion("method",
credentials: BitbucketConnectionAccessTokenCredentialsSchema.pick({ credentials: BitbucketConnectionAccessTokenCredentialsSchema.pick({
email: true email: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Bitbucket]} (API Token)` }))
]); ]);
export const ValidateBitbucketConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateBitbucketConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -65,8 +66,10 @@ export const UpdateBitbucketConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Bitbucket)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Bitbucket));
export const BitbucketConnectionListItemSchema = z.object({ export const BitbucketConnectionListItemSchema = z
name: z.literal("Bitbucket"), .object({
app: z.literal(AppConnection.Bitbucket), name: z.literal("Bitbucket"),
methods: z.nativeEnum(BitbucketConnectionMethod).array() app: z.literal(AppConnection.Bitbucket),
}); methods: z.nativeEnum(BitbucketConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Bitbucket] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { CamundaConnectionMethod } from "./camunda-connection-enums"; import { CamundaConnectionMethod } from "./camunda-connection-enums";
const BaseCamundaConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Camunda) }); const BaseCamundaConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Camunda) });
@@ -44,7 +45,7 @@ export const SanitizedCamundaConnectionSchema = z.discriminatedUnion("method", [
credentials: CamundaConnectionClientCredentialsOutputCredentialsSchema.pick({ credentials: CamundaConnectionClientCredentialsOutputCredentialsSchema.pick({
clientId: true clientId: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Camunda]} (Client Credentials)` }))
]); ]);
export const ValidateCamundaConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateCamundaConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -70,8 +71,10 @@ export const UpdateCamundaConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Camunda)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Camunda));
export const CamundaConnectionListItemSchema = z.object({ export const CamundaConnectionListItemSchema = z
name: z.literal("Camunda"), .object({
app: z.literal(AppConnection.Camunda), name: z.literal("Camunda"),
methods: z.nativeEnum(CamundaConnectionMethod).array() app: z.literal(AppConnection.Camunda),
}); methods: z.nativeEnum(CamundaConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Camunda] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { ChecklyConnectionMethod } from "./checkly-connection-constants"; import { ChecklyConnectionMethod } from "./checkly-connection-constants";
export const ChecklyConnectionMethodSchema = z export const ChecklyConnectionMethodSchema = z
@@ -31,7 +32,7 @@ export const SanitizedChecklyConnectionSchema = z.discriminatedUnion("method", [
BaseChecklyConnectionSchema.extend({ BaseChecklyConnectionSchema.extend({
method: ChecklyConnectionMethodSchema, method: ChecklyConnectionMethodSchema,
credentials: ChecklyConnectionAccessTokenCredentialsSchema.pick({}) credentials: ChecklyConnectionAccessTokenCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Checkly]} (Access Token)` }))
]); ]);
export const ValidateChecklyConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateChecklyConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -55,8 +56,10 @@ export const UpdateChecklyConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Checkly)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Checkly));
export const ChecklyConnectionListItemSchema = z.object({ export const ChecklyConnectionListItemSchema = z
name: z.literal("Checkly"), .object({
app: z.literal(AppConnection.Checkly), name: z.literal("Checkly"),
methods: z.nativeEnum(ChecklyConnectionMethod).array() app: z.literal(AppConnection.Checkly),
}); methods: z.nativeEnum(ChecklyConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Checkly] }));
@@ -9,6 +9,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { CloudflareConnectionMethod } from "./cloudflare-connection-enum"; import { CloudflareConnectionMethod } from "./cloudflare-connection-enum";
const accountIdCharacterValidator = characterValidator([ const accountIdCharacterValidator = characterValidator([
@@ -41,7 +42,7 @@ export const SanitizedCloudflareConnectionSchema = z.discriminatedUnion("method"
BaseCloudflareConnectionSchema.extend({ BaseCloudflareConnectionSchema.extend({
method: z.literal(CloudflareConnectionMethod.APIToken), method: z.literal(CloudflareConnectionMethod.APIToken),
credentials: CloudflareConnectionApiTokenCredentialsSchema.pick({ accountId: true }) credentials: CloudflareConnectionApiTokenCredentialsSchema.pick({ accountId: true })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Cloudflare]} (API Token)` }))
]); ]);
export const ValidateCloudflareConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateCloudflareConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -67,8 +68,10 @@ export const UpdateCloudflareConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Cloudflare)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Cloudflare));
export const CloudflareConnectionListItemSchema = z.object({ export const CloudflareConnectionListItemSchema = z
name: z.literal("Cloudflare"), .object({
app: z.literal(AppConnection.Cloudflare), name: z.literal("Cloudflare"),
methods: z.nativeEnum(CloudflareConnectionMethod).array() app: z.literal(AppConnection.Cloudflare),
}); methods: z.nativeEnum(CloudflareConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Cloudflare] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { DatabricksConnectionMethod } from "./databricks-connection-enums"; import { DatabricksConnectionMethod } from "./databricks-connection-enums";
export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.object({ export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.object({
@@ -42,7 +43,7 @@ export const SanitizedDatabricksConnectionSchema = z.discriminatedUnion("method"
clientId: true, clientId: true,
workspaceUrl: true workspaceUrl: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Databricks]} (Service Principal)` }))
]); ]);
export const ValidateDatabricksConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateDatabricksConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -68,10 +69,12 @@ export const UpdateDatabricksConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Databricks)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Databricks));
export const DatabricksConnectionListItemSchema = z.object({ export const DatabricksConnectionListItemSchema = z
name: z.literal("Databricks"), .object({
app: z.literal(AppConnection.Databricks), name: z.literal("Databricks"),
// the below is preferable but currently breaks with our zod to json schema parser app: z.literal(AppConnection.Databricks),
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]), // the below is preferable but currently breaks with our zod to json schema parser
methods: z.nativeEnum(DatabricksConnectionMethod).array() // methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
}); methods: z.nativeEnum(DatabricksConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Databricks] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants"; import { DigitalOceanConnectionMethod } from "./digital-ocean-connection-constants";
export const DigitalOceanConnectionMethodSchema = z export const DigitalOceanConnectionMethodSchema = z
@@ -36,7 +37,7 @@ export const SanitizedDigitalOceanConnectionSchema = z.discriminatedUnion("metho
BaseDigitalOceanConnectionSchema.extend({ BaseDigitalOceanConnectionSchema.extend({
method: DigitalOceanConnectionMethodSchema, method: DigitalOceanConnectionMethodSchema,
credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.pick({}) credentials: DigitalOceanConnectionAccessTokenCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.DigitalOcean]} (Access Token)` }))
]); ]);
export const ValidateDigitalOceanConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateDigitalOceanConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -60,8 +61,10 @@ export const UpdateDigitalOceanConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DigitalOcean)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DigitalOcean));
export const DigitalOceanConnectionListItemSchema = z.object({ export const DigitalOceanConnectionListItemSchema = z
name: z.literal("Digital Ocean"), .object({
app: z.literal(AppConnection.DigitalOcean), name: z.literal("Digital Ocean"),
methods: z.nativeEnum(DigitalOceanConnectionMethod).array() app: z.literal(AppConnection.DigitalOcean),
}); methods: z.nativeEnum(DigitalOceanConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.DigitalOcean] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { FlyioConnectionMethod } from "./flyio-connection-enums"; import { FlyioConnectionMethod } from "./flyio-connection-enums";
export const FlyioConnectionAccessTokenCredentialsSchema = z.object({ export const FlyioConnectionAccessTokenCredentialsSchema = z.object({
@@ -31,7 +32,7 @@ export const SanitizedFlyioConnectionSchema = z.discriminatedUnion("method", [
BaseFlyioConnectionSchema.extend({ BaseFlyioConnectionSchema.extend({
method: z.literal(FlyioConnectionMethod.AccessToken), method: z.literal(FlyioConnectionMethod.AccessToken),
credentials: FlyioConnectionAccessTokenCredentialsSchema.pick({}) credentials: FlyioConnectionAccessTokenCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Flyio]} (Access Token)` }))
]); ]);
export const ValidateFlyioConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateFlyioConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -55,8 +56,10 @@ export const UpdateFlyioConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Flyio)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Flyio));
export const FlyioConnectionListItemSchema = z.object({ export const FlyioConnectionListItemSchema = z
name: z.literal("Fly.io"), .object({
app: z.literal(AppConnection.Flyio), name: z.literal("Fly.io"),
methods: z.nativeEnum(FlyioConnectionMethod).array() app: z.literal(AppConnection.Flyio),
}); methods: z.nativeEnum(FlyioConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Flyio] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { GcpConnectionMethod } from "./gcp-connection-enums"; import { GcpConnectionMethod } from "./gcp-connection-enums";
export const GcpConnectionServiceAccountImpersonationCredentialsSchema = z.object({ export const GcpConnectionServiceAccountImpersonationCredentialsSchema = z.object({
@@ -30,7 +31,9 @@ export const SanitizedGcpConnectionSchema = z.discriminatedUnion("method", [
BaseGcpConnectionSchema.extend({ BaseGcpConnectionSchema.extend({
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation), method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.pick({}) credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.pick({})
}) }).describe(
JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GCP]} (Service Account Impersonation)` })
)
]); ]);
export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -56,10 +59,12 @@ export const UpdateGcpConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GCP)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GCP));
export const GcpConnectionListItemSchema = z.object({ export const GcpConnectionListItemSchema = z
name: z.literal("GCP"), .object({
app: z.literal(AppConnection.GCP), name: z.literal("GCP"),
// the below is preferable but currently breaks with our zod to json schema parser app: z.literal(AppConnection.GCP),
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]), // the below is preferable but currently breaks with our zod to json schema parser
methods: z.nativeEnum(GcpConnectionMethod).array() // methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
}); methods: z.nativeEnum(GcpConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.GCP] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums"; import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums";
export const GitHubRadarConnectionInputCredentialsSchema = z.object({ export const GitHubRadarConnectionInputCredentialsSchema = z.object({
@@ -53,14 +54,16 @@ export const SanitizedGitHubRadarConnectionSchema = z.discriminatedUnion("method
BaseGitHubRadarConnectionSchema.extend({ BaseGitHubRadarConnectionSchema.extend({
method: z.literal(GitHubRadarConnectionMethod.App), method: z.literal(GitHubRadarConnectionMethod.App),
credentials: GitHubRadarConnectionOutputCredentialsSchema.pick({}) credentials: GitHubRadarConnectionOutputCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitHubRadar]} (GitHub App)` }))
]); ]);
export const GitHubRadarConnectionListItemSchema = z.object({ export const GitHubRadarConnectionListItemSchema = z
name: z.literal("GitHub Radar"), .object({
app: z.literal(AppConnection.GitHubRadar), name: z.literal("GitHub Radar"),
// the below is preferable but currently breaks with our zod to json schema parser app: z.literal(AppConnection.GitHubRadar),
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]), // the below is preferable but currently breaks with our zod to json schema parser
methods: z.nativeEnum(GitHubRadarConnectionMethod).array(), // methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
appClientSlug: z.string().optional() methods: z.nativeEnum(GitHubRadarConnectionMethod).array(),
}); appClientSlug: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.GitHubRadar] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { GitHubConnectionMethod } from "./github-connection-enums"; import { GitHubConnectionMethod } from "./github-connection-enums";
export const GitHubConnectionOAuthInputCredentialsSchema = z.union([ export const GitHubConnectionOAuthInputCredentialsSchema = z.union([
@@ -161,29 +162,31 @@ export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(), instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
host: z.string().optional() host: z.string().optional()
}) })
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitHub]} (GitHub App)` })),
BaseGitHubConnectionSchema.extend({ BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.OAuth), method: z.literal(GitHubConnectionMethod.OAuth),
credentials: z.object({ credentials: z.object({
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(), instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
host: z.string().optional() host: z.string().optional()
}) })
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitHub]} (OAuth)` })),
BaseGitHubConnectionSchema.extend({ BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.Pat), method: z.literal(GitHubConnectionMethod.Pat),
credentials: z.object({ credentials: z.object({
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(), instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
host: z.string().optional() host: z.string().optional()
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitHub]} (Personal Access Token)` }))
]); ]);
export const GitHubConnectionListItemSchema = z.object({ export const GitHubConnectionListItemSchema = z
name: z.literal("GitHub"), .object({
app: z.literal(AppConnection.GitHub), name: z.literal("GitHub"),
// the below is preferable but currently breaks with our zod to json schema parser app: z.literal(AppConnection.GitHub),
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]), // the below is preferable but currently breaks with our zod to json schema parser
methods: z.nativeEnum(GitHubConnectionMethod).array(), // methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
oauthClientId: z.string().optional(), methods: z.nativeEnum(GitHubConnectionMethod).array(),
appClientSlug: z.string().optional() oauthClientId: z.string().optional(),
}); appClientSlug: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.GitHub] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { GitLabAccessTokenType, GitLabConnectionMethod } from "./gitlab-connection-enums"; import { GitLabAccessTokenType, GitLabConnectionMethod } from "./gitlab-connection-enums";
export const GitLabConnectionAccessTokenCredentialsSchema = z.object({ export const GitLabConnectionAccessTokenCredentialsSchema = z.object({
@@ -84,13 +85,13 @@ export const SanitizedGitLabConnectionSchema = z.discriminatedUnion("method", [
instanceUrl: true, instanceUrl: true,
accessTokenType: true accessTokenType: true
}) })
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitLab]} (Access Token)` })),
BaseGitLabConnectionSchema.extend({ BaseGitLabConnectionSchema.extend({
method: z.literal(GitLabConnectionMethod.OAuth), method: z.literal(GitLabConnectionMethod.OAuth),
credentials: GitLabConnectionOAuthOutputCredentialsSchema.pick({ credentials: GitLabConnectionOAuthOutputCredentialsSchema.pick({
instanceUrl: true instanceUrl: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.GitLab]} (OAuth)` }))
]); ]);
export const ValidateGitLabConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateGitLabConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -130,9 +131,11 @@ export const UpdateGitLabConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitLab)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitLab));
export const GitLabConnectionListItemSchema = z.object({ export const GitLabConnectionListItemSchema = z
name: z.literal("GitLab"), .object({
app: z.literal(AppConnection.GitLab), name: z.literal("GitLab"),
methods: z.nativeEnum(GitLabConnectionMethod).array(), app: z.literal(AppConnection.GitLab),
oauthClientId: z.string().optional() methods: z.nativeEnum(GitLabConnectionMethod).array(),
}); oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.GitLab] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums"; import { HCVaultConnectionMethod } from "./hc-vault-connection-enums";
const InstanceUrlSchema = z const InstanceUrlSchema = z
@@ -59,7 +60,7 @@ export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [
namespace: true, namespace: true,
instanceUrl: true instanceUrl: true
}) })
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.HCVault]} (Access Token)` })),
BaseHCVaultConnectionSchema.extend({ BaseHCVaultConnectionSchema.extend({
method: z.literal(HCVaultConnectionMethod.AppRole), method: z.literal(HCVaultConnectionMethod.AppRole),
credentials: HCVaultConnectionAppRoleCredentialsSchema.pick({ credentials: HCVaultConnectionAppRoleCredentialsSchema.pick({
@@ -67,7 +68,7 @@ export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [
instanceUrl: true, instanceUrl: true,
roleId: true roleId: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.HCVault]} (App Role)` }))
]); ]);
export const ValidateHCVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateHCVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -100,8 +101,10 @@ export const UpdateHCVaultConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault, { supportsGateways: true })); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault, { supportsGateways: true }));
export const HCVaultConnectionListItemSchema = z.object({ export const HCVaultConnectionListItemSchema = z
name: z.literal("HCVault"), .object({
app: z.literal(AppConnection.HCVault), name: z.literal("HCVault"),
methods: z.nativeEnum(HCVaultConnectionMethod).array() app: z.literal(AppConnection.HCVault),
}); methods: z.nativeEnum(HCVaultConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.HCVault] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { HerokuConnectionMethod } from "./heroku-connection-enums"; import { HerokuConnectionMethod } from "./heroku-connection-enums";
export const HerokuConnectionAuthTokenCredentialsSchema = z.object({ export const HerokuConnectionAuthTokenCredentialsSchema = z.object({
@@ -51,11 +52,11 @@ export const SanitizedHerokuConnectionSchema = z.discriminatedUnion("method", [
BaseHerokuConnectionSchema.extend({ BaseHerokuConnectionSchema.extend({
method: z.literal(HerokuConnectionMethod.AuthToken), method: z.literal(HerokuConnectionMethod.AuthToken),
credentials: HerokuConnectionAuthTokenCredentialsSchema.pick({}) credentials: HerokuConnectionAuthTokenCredentialsSchema.pick({})
}), }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Heroku]} (Auth Token)` })),
BaseHerokuConnectionSchema.extend({ BaseHerokuConnectionSchema.extend({
method: z.literal(HerokuConnectionMethod.OAuth), method: z.literal(HerokuConnectionMethod.OAuth),
credentials: HerokuConnectionOAuthOutputCredentialsSchema.pick({}) credentials: HerokuConnectionOAuthOutputCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Heroku]} (OAuth)` }))
]); ]);
export const ValidateHerokuConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateHerokuConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -95,9 +96,11 @@ export const UpdateHerokuConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Heroku)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Heroku));
export const HerokuConnectionListItemSchema = z.object({ export const HerokuConnectionListItemSchema = z
name: z.literal("Heroku"), .object({
app: z.literal(AppConnection.Heroku), name: z.literal("Heroku"),
methods: z.nativeEnum(HerokuConnectionMethod).array(), app: z.literal(AppConnection.Heroku),
oauthClientId: z.string().optional() methods: z.nativeEnum(HerokuConnectionMethod).array(),
}); oauthClientId: z.string().optional()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Heroku] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { HumanitecConnectionMethod } from "./humanitec-connection-enums"; import { HumanitecConnectionMethod } from "./humanitec-connection-enums";
export const HumanitecConnectionAccessTokenCredentialsSchema = z.object({ export const HumanitecConnectionAccessTokenCredentialsSchema = z.object({
@@ -25,7 +26,7 @@ export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method",
BaseHumanitecConnectionSchema.extend({ BaseHumanitecConnectionSchema.extend({
method: z.literal(HumanitecConnectionMethod.ApiToken), method: z.literal(HumanitecConnectionMethod.ApiToken),
credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({}) credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Humanitec]} (API Token)` }))
]); ]);
export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -51,8 +52,10 @@ export const UpdateHumanitecConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Humanitec)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Humanitec));
export const HumanitecConnectionListItemSchema = z.object({ export const HumanitecConnectionListItemSchema = z
name: z.literal("Humanitec"), .object({
app: z.literal(AppConnection.Humanitec), name: z.literal("Humanitec"),
methods: z.nativeEnum(HumanitecConnectionMethod).array() app: z.literal(AppConnection.Humanitec),
}); methods: z.nativeEnum(HumanitecConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Humanitec] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { LaravelForgeConnectionMethod } from "./laravel-forge-connection-enums"; import { LaravelForgeConnectionMethod } from "./laravel-forge-connection-enums";
export const LaravelForgeConnectionApiTokenCredentialsSchema = z.object({ export const LaravelForgeConnectionApiTokenCredentialsSchema = z.object({
@@ -25,7 +26,7 @@ export const SanitizedLaravelForgeConnectionSchema = z.discriminatedUnion("metho
BaseLaravelForgeConnectionSchema.extend({ BaseLaravelForgeConnectionSchema.extend({
method: z.literal(LaravelForgeConnectionMethod.ApiToken), method: z.literal(LaravelForgeConnectionMethod.ApiToken),
credentials: LaravelForgeConnectionApiTokenCredentialsSchema.pick({}) credentials: LaravelForgeConnectionApiTokenCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.LaravelForge]} (API Token)` }))
]); ]);
export const ValidateLaravelForgeConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateLaravelForgeConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -51,8 +52,10 @@ export const UpdateLaravelForgeConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LaravelForge)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LaravelForge));
export const LaravelForgeConnectionListItemSchema = z.object({ export const LaravelForgeConnectionListItemSchema = z
name: z.literal("Laravel Forge"), .object({
app: z.literal(AppConnection.LaravelForge), name: z.literal("Laravel Forge"),
methods: z.nativeEnum(LaravelForgeConnectionMethod).array() app: z.literal(AppConnection.LaravelForge),
}); methods: z.nativeEnum(LaravelForgeConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.LaravelForge] }));
@@ -9,6 +9,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums"; import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
export const LdapConnectionSimpleBindCredentialsSchema = z.object({ export const LdapConnectionSimpleBindCredentialsSchema = z.object({
@@ -61,7 +62,7 @@ export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [
sslRejectUnauthorized: true, sslRejectUnauthorized: true,
sslCertificate: true sslCertificate: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.LDAP]} (Simple Bind)` }))
]); ]);
export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -91,10 +92,12 @@ export const UpdateLdapConnectionSchema = z
}) })
); );
export const LdapConnectionListItemSchema = z.object({ export const LdapConnectionListItemSchema = z
name: z.literal("LDAP"), .object({
app: z.literal(AppConnection.LDAP), name: z.literal("LDAP"),
// the below is preferable but currently breaks with our zod to json schema parser app: z.literal(AppConnection.LDAP),
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]), // the below is preferable but currently breaks with our zod to json schema parser
methods: z.nativeEnum(LdapConnectionMethod).array() // methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
}); methods: z.nativeEnum(LdapConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.LDAP] }));
@@ -8,6 +8,7 @@ import {
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql"; import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql";
import { MsSqlConnectionMethod } from "./mssql-connection-enums"; import { MsSqlConnectionMethod } from "./mssql-connection-enums";
@@ -34,7 +35,7 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
sslRejectUnauthorized: true, sslRejectUnauthorized: true,
sslCertificate: true sslCertificate: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.MsSql]} (Username and Password)` }))
]); ]);
export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -68,9 +69,11 @@ export const UpdateMsSqlConnectionSchema = z
}) })
); );
export const MsSqlConnectionListItemSchema = z.object({ export const MsSqlConnectionListItemSchema = z
name: z.literal("Microsoft SQL Server"), .object({
app: z.literal(AppConnection.MsSql), name: z.literal("Microsoft SQL Server"),
methods: z.nativeEnum(MsSqlConnectionMethod).array(), app: z.literal(AppConnection.MsSql),
supportsPlatformManagement: z.literal(true) methods: z.nativeEnum(MsSqlConnectionMethod).array(),
}); supportsPlatformManagement: z.literal(true)
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.MsSql] }));
@@ -8,6 +8,7 @@ import {
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql"; import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql";
import { MySqlConnectionMethod } from "./mysql-connection-enums"; import { MySqlConnectionMethod } from "./mysql-connection-enums";
@@ -32,7 +33,7 @@ export const SanitizedMySqlConnectionSchema = z.discriminatedUnion("method", [
sslRejectUnauthorized: true, sslRejectUnauthorized: true,
sslCertificate: true sslCertificate: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.MySql]} (Username and Password)` }))
]); ]);
export const ValidateMySqlConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateMySqlConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -66,9 +67,11 @@ export const UpdateMySqlConnectionSchema = z
}) })
); );
export const MySqlConnectionListItemSchema = z.object({ export const MySqlConnectionListItemSchema = z
name: z.literal("MySQL"), .object({
app: z.literal(AppConnection.MySql), name: z.literal("MySQL"),
methods: z.nativeEnum(MySqlConnectionMethod).array(), app: z.literal(AppConnection.MySql),
supportsPlatformManagement: z.literal(true) methods: z.nativeEnum(MySqlConnectionMethod).array(),
}); supportsPlatformManagement: z.literal(true)
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.MySql] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { NetlifyConnectionMethod } from "./netlify-connection-constants"; import { NetlifyConnectionMethod } from "./netlify-connection-constants";
export const NetlifyConnectionMethodSchema = z export const NetlifyConnectionMethodSchema = z
@@ -36,7 +37,7 @@ export const SanitizedNetlifyConnectionSchema = z.discriminatedUnion("method", [
BaseNetlifyConnectionSchema.extend({ BaseNetlifyConnectionSchema.extend({
method: NetlifyConnectionMethodSchema, method: NetlifyConnectionMethodSchema,
credentials: NetlifyConnectionAccessTokenCredentialsSchema.pick({}) credentials: NetlifyConnectionAccessTokenCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Netlify]} (Access Token)` }))
]); ]);
export const ValidateNetlifyConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateNetlifyConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -60,8 +61,10 @@ export const UpdateNetlifyConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Netlify)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Netlify));
export const NetlifyConnectionListItemSchema = z.object({ export const NetlifyConnectionListItemSchema = z
name: z.literal("Netlify"), .object({
app: z.literal(AppConnection.Netlify), name: z.literal("Netlify"),
methods: z.nativeEnum(NetlifyConnectionMethod).array() app: z.literal(AppConnection.Netlify),
}); methods: z.nativeEnum(NetlifyConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Netlify] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { NorthflankConnectionMethod } from "./northflank-connection-enums"; import { NorthflankConnectionMethod } from "./northflank-connection-enums";
export const NorthflankConnectionApiTokenCredentialsSchema = z.object({ export const NorthflankConnectionApiTokenCredentialsSchema = z.object({
@@ -27,7 +28,7 @@ export const SanitizedNorthflankConnectionSchema = z.discriminatedUnion("method"
BaseNorthflankConnectionSchema.extend({ BaseNorthflankConnectionSchema.extend({
method: z.literal(NorthflankConnectionMethod.ApiToken), method: z.literal(NorthflankConnectionMethod.ApiToken),
credentials: NorthflankConnectionApiTokenCredentialsSchema.pick({}) credentials: NorthflankConnectionApiTokenCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Northflank]} (API Token)` }))
]); ]);
export const ValidateNorthflankConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateNorthflankConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -53,8 +54,10 @@ export const UpdateNorthflankConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Northflank)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Northflank));
export const NorthflankConnectionListItemSchema = z.object({ export const NorthflankConnectionListItemSchema = z
name: z.literal("Northflank"), .object({
app: z.literal(AppConnection.Northflank), name: z.literal("Northflank"),
methods: z.nativeEnum(NorthflankConnectionMethod).array() app: z.literal(AppConnection.Northflank),
}); methods: z.nativeEnum(NorthflankConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Northflank] }));
@@ -9,6 +9,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { OktaConnectionMethod } from "./okta-connection-enums"; import { OktaConnectionMethod } from "./okta-connection-enums";
export const OktaConnectionApiTokenCredentialsSchema = z.object({ export const OktaConnectionApiTokenCredentialsSchema = z.object({
@@ -40,7 +41,7 @@ export const SanitizedOktaConnectionSchema = z.discriminatedUnion("method", [
credentials: OktaConnectionApiTokenCredentialsSchema.pick({ credentials: OktaConnectionApiTokenCredentialsSchema.pick({
instanceUrl: true instanceUrl: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Okta]} (API Token)` }))
]); ]);
export const ValidateOktaConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateOktaConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -62,8 +63,10 @@ export const UpdateOktaConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Okta)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Okta));
export const OktaConnectionListItemSchema = z.object({ export const OktaConnectionListItemSchema = z
name: z.literal("Okta"), .object({
app: z.literal(AppConnection.Okta), name: z.literal("Okta"),
methods: z.nativeEnum(OktaConnectionMethod).array() app: z.literal(AppConnection.Okta),
}); methods: z.nativeEnum(OktaConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Okta] }));
@@ -8,6 +8,7 @@ import {
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql"; import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql";
import { PostgresConnectionMethod } from "./postgres-connection-enums"; import { PostgresConnectionMethod } from "./postgres-connection-enums";
@@ -32,7 +33,7 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method",
sslRejectUnauthorized: true, sslRejectUnauthorized: true,
sslCertificate: true sslCertificate: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Postgres]} (Username and Password)` }))
]); ]);
export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -66,9 +67,11 @@ export const UpdatePostgresConnectionSchema = z
}) })
); );
export const PostgresConnectionListItemSchema = z.object({ export const PostgresConnectionListItemSchema = z
name: z.literal("PostgreSQL"), .object({
app: z.literal(AppConnection.Postgres), name: z.literal("PostgreSQL"),
methods: z.nativeEnum(PostgresConnectionMethod).array(), app: z.literal(AppConnection.Postgres),
supportsPlatformManagement: z.literal(true) methods: z.nativeEnum(PostgresConnectionMethod).array(),
}); supportsPlatformManagement: z.literal(true)
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Postgres] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { RailwayConnectionMethod } from "./railway-connection-constants"; import { RailwayConnectionMethod } from "./railway-connection-constants";
export const RailwayConnectionMethodSchema = z export const RailwayConnectionMethodSchema = z
@@ -36,7 +37,7 @@ export const SanitizedRailwayConnectionSchema = z.discriminatedUnion("method", [
BaseRailwayConnectionSchema.extend({ BaseRailwayConnectionSchema.extend({
method: RailwayConnectionMethodSchema, method: RailwayConnectionMethodSchema,
credentials: RailwayConnectionAccessTokenCredentialsSchema.pick({}) credentials: RailwayConnectionAccessTokenCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Railway]} (Access Token)` }))
]); ]);
export const ValidateRailwayConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateRailwayConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -60,11 +61,13 @@ export const UpdateRailwayConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Railway)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Railway));
export const RailwayConnectionListItemSchema = z.object({ export const RailwayConnectionListItemSchema = z
name: z.literal("Railway"), .object({
app: z.literal(AppConnection.Railway), name: z.literal("Railway"),
methods: z.nativeEnum(RailwayConnectionMethod).array() app: z.literal(AppConnection.Railway),
}); methods: z.nativeEnum(RailwayConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Railway] }));
export const RailwayResourceSchema = z.object({ export const RailwayResourceSchema = z.object({
node: z.object({ node: z.object({
@@ -8,6 +8,7 @@ import {
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { RedisConnectionMethod } from "./redis-connection-enums"; import { RedisConnectionMethod } from "./redis-connection-enums";
export const BaseRedisUsernameAndPasswordConnectionSchema = z.object({ export const BaseRedisUsernameAndPasswordConnectionSchema = z.object({
@@ -45,7 +46,7 @@ export const SanitizedRedisConnectionSchema = z.discriminatedUnion("method", [
sslRejectUnauthorized: true, sslRejectUnauthorized: true,
sslCertificate: true sslCertificate: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Redis]} (Username and Password)` }))
]); ]);
export const ValidateRedisConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateRedisConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -79,9 +80,11 @@ export const UpdateRedisConnectionSchema = z
}) })
); );
export const RedisConnectionListItemSchema = z.object({ export const RedisConnectionListItemSchema = z
name: z.literal("Redis"), .object({
app: z.literal(AppConnection.Redis), name: z.literal("Redis"),
methods: z.nativeEnum(RedisConnectionMethod).array(), app: z.literal(AppConnection.Redis),
supportsPlatformManagement: z.literal(false) methods: z.nativeEnum(RedisConnectionMethod).array(),
}); supportsPlatformManagement: z.literal(false)
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Redis] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { RenderConnectionMethod } from "./render-connection-enums"; import { RenderConnectionMethod } from "./render-connection-enums";
export const RenderConnectionApiKeyCredentialsSchema = z.object({ export const RenderConnectionApiKeyCredentialsSchema = z.object({
@@ -25,7 +26,7 @@ export const SanitizedRenderConnectionSchema = z.discriminatedUnion("method", [
BaseRenderConnectionSchema.extend({ BaseRenderConnectionSchema.extend({
method: z.literal(RenderConnectionMethod.ApiKey), method: z.literal(RenderConnectionMethod.ApiKey),
credentials: RenderConnectionApiKeyCredentialsSchema.pick({}) credentials: RenderConnectionApiKeyCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Render]} (API Key)` }))
]); ]);
export const ValidateRenderConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateRenderConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -49,8 +50,10 @@ export const UpdateRenderConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Render)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Render));
export const RenderConnectionListItemSchema = z.object({ export const RenderConnectionListItemSchema = z
name: z.literal("Render"), .object({
app: z.literal(AppConnection.Render), name: z.literal("Render"),
methods: z.nativeEnum(RenderConnectionMethod).array() app: z.literal(AppConnection.Render),
}); methods: z.nativeEnum(RenderConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Render] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { SupabaseConnectionMethod } from "./supabase-connection-constants"; import { SupabaseConnectionMethod } from "./supabase-connection-constants";
export const SupabaseConnectionMethodSchema = z export const SupabaseConnectionMethodSchema = z
@@ -39,7 +40,7 @@ export const SanitizedSupabaseConnectionSchema = z.discriminatedUnion("method",
credentials: SupabaseConnectionAccessTokenCredentialsSchema.pick({ credentials: SupabaseConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true instanceUrl: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.Supabase]} (Access Token)` }))
]); ]);
export const ValidateSupabaseConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateSupabaseConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -63,8 +64,10 @@ export const UpdateSupabaseConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Supabase)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Supabase));
export const SupabaseConnectionListItemSchema = z.object({ export const SupabaseConnectionListItemSchema = z
name: z.literal("Supabase"), .object({
app: z.literal(AppConnection.Supabase), name: z.literal("Supabase"),
methods: z.nativeEnum(SupabaseConnectionMethod).array() app: z.literal(AppConnection.Supabase),
}); methods: z.nativeEnum(SupabaseConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.Supabase] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { TeamCityConnectionMethod } from "./teamcity-connection-enums"; import { TeamCityConnectionMethod } from "./teamcity-connection-enums";
export const TeamCityConnectionAccessTokenCredentialsSchema = z.object({ export const TeamCityConnectionAccessTokenCredentialsSchema = z.object({
@@ -37,7 +38,7 @@ export const SanitizedTeamCityConnectionSchema = z.discriminatedUnion("method",
credentials: TeamCityConnectionAccessTokenCredentialsSchema.pick({ credentials: TeamCityConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true instanceUrl: true
}) })
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.TeamCity]} (Access Token)` }))
]); ]);
export const ValidateTeamCityConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateTeamCityConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -63,8 +64,10 @@ export const UpdateTeamCityConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TeamCity)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TeamCity));
export const TeamCityConnectionListItemSchema = z.object({ export const TeamCityConnectionListItemSchema = z
name: z.literal("TeamCity"), .object({
app: z.literal(AppConnection.TeamCity), name: z.literal("TeamCity"),
methods: z.nativeEnum(TeamCityConnectionMethod).array() app: z.literal(AppConnection.TeamCity),
}); methods: z.nativeEnum(TeamCityConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.TeamCity] }));
@@ -8,6 +8,7 @@ import {
GenericUpdateAppConnectionFieldsSchema GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas"; } from "@app/services/app-connection/app-connection-schemas";
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
import { TerraformCloudConnectionMethod } from "./terraform-cloud-connection-enums"; import { TerraformCloudConnectionMethod } from "./terraform-cloud-connection-enums";
export const TerraformCloudConnectionAccessTokenCredentialsSchema = z.object({ export const TerraformCloudConnectionAccessTokenCredentialsSchema = z.object({
@@ -27,7 +28,7 @@ export const SanitizedTerraformCloudConnectionSchema = z.discriminatedUnion("met
BaseTerraformCloudConnectionSchema.extend({ BaseTerraformCloudConnectionSchema.extend({
method: z.literal(TerraformCloudConnectionMethod.ApiToken), method: z.literal(TerraformCloudConnectionMethod.ApiToken),
credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.pick({}) credentials: TerraformCloudConnectionAccessTokenCredentialsSchema.pick({})
}) }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.TerraformCloud]} (API Token)` }))
]); ]);
export const ValidateTerraformCloudConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateTerraformCloudConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -53,8 +54,10 @@ export const UpdateTerraformCloudConnectionSchema = z
}) })
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TerraformCloud)); .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TerraformCloud));
export const TerraformCloudConnectionListItemSchema = z.object({ export const TerraformCloudConnectionListItemSchema = z
name: z.literal("Terraform Cloud"), .object({
app: z.literal(AppConnection.TerraformCloud), name: z.literal("Terraform Cloud"),
methods: z.nativeEnum(TerraformCloudConnectionMethod).array() app: z.literal(AppConnection.TerraformCloud),
}); methods: z.nativeEnum(TerraformCloudConnectionMethod).array()
})
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.TerraformCloud] }));

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