mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 13:28:34 +00:00
Merge branch 'main' into feature/ldap-gateway-support
This commit is contained in:
+1
-1
@@ -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,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=
|
||||||
|
|||||||
@@ -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 -->
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 |
|
||||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||||
|
|||||||
@@ -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 |
|
||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
|
"""
|
||||||
@@ -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} |
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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-----
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBx7d0VqxwTYcJajFgz
|
||||||
|
ja0PExBmxdZjEQRfGCMQY8GfHa0WpBUEwVtBD6XOGE5xZB2hZANiAATZXmgzkmKa
|
||||||
|
oOPMR8fXRwq7ovx5D2vHORQtsglEUW65e0pBDCRwBbwvhS4ySYaW7mEUIFHyxocO
|
||||||
|
khK4DJgtiqx13HCd3GYZPfPLDqhT7yhdZfS1OEvQqBqOZlyQ59HKxik=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDnPx90G0J4ba0CMTrh
|
||||||
|
AT0kJkRGyhv5ePWyobdT75za/I9MpU/VsC8BG5uJBraxiSOhZANiAAQWEiTINq0t
|
||||||
|
j+6Qiyzin74FU4/zLNuEs1FnipFn+Vb1W8qhvbBwLOGsANpaHIg4dpR+CghfccRQ
|
||||||
|
0kQm/AMgj08VXvta6vV7aQ8yk+/Cp6l4SVQ9GzizHiJ//Qb71vrXbco=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -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-----
|
||||||
Generated
+97
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Vendored
+3
-1
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 +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" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 || ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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] }));
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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" };
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// };
|
||||||
+5
-3
@@ -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,
|
||||||
|
|||||||
@@ -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] }));
|
||||||
|
|||||||
+13
-8
@@ -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] }));
|
||||||
|
|||||||
+14
-9
@@ -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] }));
|
||||||
|
|||||||
+11
-8
@@ -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] }));
|
||||||
|
|||||||
+9
-6
@@ -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] }));
|
||||||
|
|||||||
+12
-9
@@ -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] }));
|
||||||
|
|||||||
+9
-6
@@ -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] }));
|
||||||
|
|||||||
+9
-6
@@ -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
Reference in New Issue
Block a user