mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2026-06-10 08:36:40 +02:00
(docs): consolidate docs into agents.md; fix operator health, ha, and e2e notes for 3.5.2
This commit is contained in:
parent
4543cfd02b
commit
06ba5e8e2c
17
.cursor/maksit-skills.json
Normal file
17
.cursor/maksit-skills.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "homelab-maksit-skills-manifest-v2",
|
||||||
|
"sharedSkillsRoot": "E:\\Users\\maksym\\source\\repos\\private\\homelab\\ai\\skills",
|
||||||
|
"sharedSkills": [
|
||||||
|
"common/csharp",
|
||||||
|
"common/maksit-layering",
|
||||||
|
"common/maksit-auth-rbac",
|
||||||
|
"common/maksit-identity-tokens",
|
||||||
|
"common/maksit-patch",
|
||||||
|
"common/maksit-ha",
|
||||||
|
"common/maksit-reverse-proxy",
|
||||||
|
"common/react-typescript",
|
||||||
|
"common/helm",
|
||||||
|
"common/maksit-repo-maintenance",
|
||||||
|
"local-ollama"
|
||||||
|
]
|
||||||
|
}
|
||||||
23
.cursor/rules/maksit-skills.mdc
Normal file
23
.cursor/rules/maksit-skills.mdc
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
description: Load MaksIT agent skills from homelab common/
|
||||||
|
globs: "**/*.{cs,csproj,slnx,ts,tsx,json,md,ps1,yml,yaml}"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# MaksIT skills (maksit-certs-ui)
|
||||||
|
|
||||||
|
Complementary skills (no precedence). Read each `SKILL.md` when relevant:
|
||||||
|
|
||||||
|
1. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\csharp\SKILL.md`
|
||||||
|
2. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-layering\SKILL.md`
|
||||||
|
3. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-auth-rbac\SKILL.md`
|
||||||
|
4. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-identity-tokens\SKILL.md`
|
||||||
|
5. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-patch\SKILL.md`
|
||||||
|
6. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-ha\SKILL.md`
|
||||||
|
7. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-reverse-proxy\SKILL.md`
|
||||||
|
8. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\react-typescript\SKILL.md`
|
||||||
|
9. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\helm\SKILL.md`
|
||||||
|
10. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-repo-maintenance\SKILL.md`
|
||||||
|
11. `E:\Users\maksym\source\repos\private\homelab\ai\skills\local-ollama\SKILL.md` — local Ollama offload (`@local-ollama`)
|
||||||
|
|
||||||
|
Manifest: `.cursor/maksit-skills.json`.
|
||||||
21
AGENTS.md
Normal file
21
AGENTS.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Agent instructions (maksit-certs-ui)
|
||||||
|
|
||||||
|
Complementary skills (no precedence). Homelab paths use `sharedSkills` in `.cursor/maksit-skills.json`.
|
||||||
|
|
||||||
|
| Skill | Path |
|
||||||
|
|-------|------|
|
||||||
|
| csharp | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\csharp\SKILL.md) |
|
||||||
|
| maksit-layering | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-layering\SKILL.md) |
|
||||||
|
| maksit-auth-rbac | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-auth-rbac\SKILL.md) |
|
||||||
|
| maksit-identity-tokens | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-identity-tokens\SKILL.md) |
|
||||||
|
| maksit-patch | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-patch\SKILL.md) |
|
||||||
|
| maksit-ha | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-ha\SKILL.md) |
|
||||||
|
| maksit-reverse-proxy | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-reverse-proxy\SKILL.md) |
|
||||||
|
| react-typescript | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\react-typescript\SKILL.md) |
|
||||||
|
| helm | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\helm\SKILL.md) |
|
||||||
|
| maksit-repo-maintenance | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-repo-maintenance\SKILL.md) |
|
||||||
|
| local-ollama | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\local-ollama\SKILL.md) |
|
||||||
|
|
||||||
|
Manifest: `.cursor/maksit-skills.json`.
|
||||||
|
|
||||||
|
**Contributors:** pick the skill that matches the task; **source code** is authoritative.
|
||||||
10
CHANGELOG.md
10
CHANGELOG.md
@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.5.2] - 2026-06-04
|
||||||
|
|
||||||
|
**Release status:** **3.3.4** is the last published release. **3.5.2** is a patch on **3.5.1** (documentation consolidation, dependency bumps).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Documentation:** Removed **`assets/docs/`** topic guides (layering, HA, auth, RBAC, proxy, patch, PowerShell); README and CONTRIBUTING point contributors at **[AGENTS.md](AGENTS.md)** (homelab `common/maksit-*` skills); operator-facing health and HA notes stay in README and Helm **`NOTES.txt`**.
|
||||||
|
- **Helm:** **`NOTES.txt`** health probe note is self-contained (no link to removed HA doc).
|
||||||
|
- **Dependencies:** **MaksIT.Core** **1.6.7**, **MaksIT.Results** **2.0.2** (host, Engine, Client.Tests).
|
||||||
|
|
||||||
## [3.5.1] - 2026-06-02
|
## [3.5.1] - 2026-06-02
|
||||||
|
|
||||||
**Release status:** **3.3.4** is the last published release. **3.5.1** is a patch on **3.5.0** (startup health, Helm secrets alignment, RBAC docs, Web UI token refresh).
|
**Release status:** **3.3.4** is the last published release. **3.5.1** is a patch on **3.5.0** (startup health, Helm secrets alignment, RBAC docs, Web UI token refresh).
|
||||||
|
|||||||
@ -14,9 +14,9 @@ Large or architectural changes are best discussed first (see [Contact](#contact)
|
|||||||
|
|
||||||
## Architecture and code layout
|
## Architecture and code layout
|
||||||
|
|
||||||
**Where the rules live:** layering, folder responsibilities, persistence vs host boundaries, DI lifetimes, and an AI/contributor checklist are documented in **[assets/docs/ARCHITECTURE_LAYERING.md](assets/docs/ARCHITECTURE_LAYERING.md)**. Read that before adding new Engine persistence, services, or cross-project dependencies.
|
**Canonical (contributors / AI agents):** [AGENTS.md](AGENTS.md) — homelab shared skills via `.cursor/maksit-skills.json`.
|
||||||
|
|
||||||
**Summary:** `MaksIT.CertsUI.Engine` holds domain, PostgreSQL persistence (Linq2Db), migrations, and ACME engine code; it returns `MaksIT.Results` types, not HTTP responses. `MaksIT.CertsUI` is the web host (controllers, app services, ProblemDetails). Topic-specific design notes also live under [assets/docs/](assets/docs/) (HA, auth, RBAC, proxy, etc.). For authorization work, read [USER_AND_API_KEY_RBAC.md](assets/docs/USER_AND_API_KEY_RBAC.md) and [RBAC_REFERENCE.md](assets/docs/RBAC_REFERENCE.md) before changing filters or identity/API-key services.
|
**Summary:** `MaksIT.CertsUI.Engine` holds domain, PostgreSQL persistence (Linq2Db), migrations, and ACME engine code; it returns `MaksIT.Results` types, not HTTP responses. `MaksIT.CertsUI` is the web host (controllers, app services, ProblemDetails). Before changing filters or identity/API-key services, read **maksit-auth-rbac** and **maksit-layering** (listed in AGENTS.md).
|
||||||
|
|
||||||
## Development setup
|
## Development setup
|
||||||
|
|
||||||
|
|||||||
93
README.md
93
README.md
@ -6,9 +6,9 @@ MaksIT.CertsUI is a powerful, container-native ACMEv2 client built to simplify a
|
|||||||
|
|
||||||
Designed for modern infrastructure, it combines a robust WebAPI, intuitive WebUI, and lightweight edge Agent to deliver fully automated certificate issuance, renewal, and deployment across Docker, Podman, and Kubernetes environments. MaksIT.CertsUI supports the HTTP-01 challenge and follows the official [Let’s Encrypt guidelines](https://letsencrypt.org/docs/) while implementing recommended security and operational best practices.
|
Designed for modern infrastructure, it combines a robust WebAPI, intuitive WebUI, and lightweight edge Agent to deliver fully automated certificate issuance, renewal, and deployment across Docker, Podman, and Kubernetes environments. MaksIT.CertsUI supports the HTTP-01 challenge and follows the official [Let’s Encrypt guidelines](https://letsencrypt.org/docs/) while implementing recommended security and operational best practices.
|
||||||
|
|
||||||
Authorization is **scope-based RBAC** for **users** and **API keys** (organization-scoped **Identity** / **ApiKey** flags). **Global administrator** on a signed-in user (JWT) and on an API key are evaluated **separately**—a user being admin does not automatically grant the same to a key they create. Certificate and account endpoints today accept **any authenticated** principal; see the matrices for detail.
|
Authorization is **scope-based RBAC** for **users** and **API keys** (organization-scoped **Identity** / **ApiKey** flags). **Global administrator** on a signed-in user (JWT) and on an API key are evaluated **separately**—a user being admin does not automatically grant the same to a key they create. Certificate and account endpoints today accept **any authenticated** principal; enforcement is in **`src/MaksIT.CertsUI/`** (authorization filter, identity, and API-key services). Contributors: **AGENTS.md** → **maksit-auth-rbac**.
|
||||||
|
|
||||||
Permission matrices and scope semantics are documented in the [RBAC reference](assets/docs/RBAC_REFERENCE.md); authentication mechanics and routes are in [User and API key RBAC](assets/docs/USER_AND_API_KEY_RBAC.md).
|
Contributors and AI agents: architecture skill index in **[AGENTS.md](AGENTS.md)** (homelab `common/maksit-*` skills via `.cursor/maksit-skills.json`). **Source code** is authoritative when docs and behavior differ.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -26,12 +26,7 @@ If you find this project useful, please consider supporting its development:
|
|||||||
- [Table of Contents](#table-of-contents)
|
- [Table of Contents](#table-of-contents)
|
||||||
- [Changelog](#changelog)
|
- [Changelog](#changelog)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
- [User and API key RBAC](#user-and-api-key-rbac)
|
- [Architecture and documentation](#architecture-and-documentation)
|
||||||
- [RBAC reference](#rbac-reference)
|
|
||||||
- [Patch and delta reference](#patch-and-delta-reference)
|
|
||||||
- [Login and refresh token architecture](#login-and-refresh-token-architecture)
|
|
||||||
- [Reverse proxy routing (YARP)](#reverse-proxy-routing-yarp)
|
|
||||||
- [High availability architecture](#high-availability-architecture)
|
|
||||||
- [Architecture](#architecture)
|
- [Architecture](#architecture)
|
||||||
- [Current Limitations](#current-limitations)
|
- [Current Limitations](#current-limitations)
|
||||||
- [Architecture Scheme](#architecture-scheme)
|
- [Architecture Scheme](#architecture-scheme)
|
||||||
@ -66,81 +61,19 @@ Version history and release notes live in [CHANGELOG.md](CHANGELOG.md).
|
|||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, pull request expectations, and security reporting.
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, pull request expectations, and security reporting.
|
||||||
|
|
||||||
## User and API key RBAC
|
## Architecture and documentation
|
||||||
|
|
||||||
How JWT and **`X-API-KEY`** principals are resolved, how **`CertsUIAuthorizationFilter`** differs from Vault’s route split, **`GetActingJwtTokenData`**, and where rules live in code: **[assets/docs/USER_AND_API_KEY_RBAC.md](assets/docs/USER_AND_API_KEY_RBAC.md)**.
|
**Contributors / AI agents:** skill index in **[AGENTS.md](AGENTS.md)** (homelab `common/maksit-*` skills — layering, RBAC, HA, Helm, etc.). **Operators:** runbooks and behavior below, [CHANGELOG.md](CHANGELOG.md), and Helm **`NOTES.txt`** after install.
|
||||||
|
|
||||||
- [1. Two authentication mechanisms](assets/docs/USER_AND_API_KEY_RBAC.md#1-two-authentication-mechanisms)
|
**Server health endpoints:**
|
||||||
- [2. Two principal types (what RBAC sees)](assets/docs/USER_AND_API_KEY_RBAC.md#2-two-principal-types-what-rbac-sees)
|
|
||||||
- [2.1 Global administrator: user vs key](assets/docs/USER_AND_API_KEY_RBAC.md#21-global-administrator-user-vs-key-easy-to-confuse)
|
|
||||||
- [2.2 Loading API key authorization](assets/docs/USER_AND_API_KEY_RBAC.md#22-loading-api-key-authorization)
|
|
||||||
- [3. Shared RBAC helpers (`ServiceBase`)](assets/docs/USER_AND_API_KEY_RBAC.md#3-shared-rbac-helpers-servicebase)
|
|
||||||
- [4. Example: accounts and ACME](assets/docs/USER_AND_API_KEY_RBAC.md#4-example-accounts-and-acme-accountservice-certsflowservice)
|
|
||||||
- [5. Identity and API key administration](assets/docs/USER_AND_API_KEY_RBAC.md#5-identity-and-api-key-administration-getactingjwttokendata)
|
|
||||||
- [6. Troubleshooting](assets/docs/USER_AND_API_KEY_RBAC.md#6-troubleshooting)
|
|
||||||
- [7. Code map](assets/docs/USER_AND_API_KEY_RBAC.md#7-code-map)
|
|
||||||
|
|
||||||
## RBAC reference
|
| Path | Use |
|
||||||
|
|------|-----|
|
||||||
|
| `GET /health/live` | Liveness — process up |
|
||||||
|
| `GET /health/ready` | Readiness and load balancers — **503** until bootstrap coordination completes, then PostgreSQL check |
|
||||||
|
| `GET /health/startup` | Startup diagnostics — JSON phase snapshot (migrations, schema sync, bootstrap) |
|
||||||
|
|
||||||
Scope flags, intended vs enforced rules, and permission matrices for Identity, API keys, and ACME endpoints: **[assets/docs/RBAC_REFERENCE.md](assets/docs/RBAC_REFERENCE.md)**.
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup.
|
||||||
|
|
||||||
- [1. Scope model](assets/docs/RBAC_REFERENCE.md#1-scope-model)
|
|
||||||
- [2. Shorthand columns (matrices below)](assets/docs/RBAC_REFERENCE.md#2-shorthand-columns-matrices-below)
|
|
||||||
- [3. Global administrator](assets/docs/RBAC_REFERENCE.md#3-global-administrator)
|
|
||||||
- [4. Identity (users)](assets/docs/RBAC_REFERENCE.md#4-identity-users)
|
|
||||||
- [4.1 Enforced in code today](assets/docs/RBAC_REFERENCE.md#41-enforced-in-code-today-source-of-truth)
|
|
||||||
- [4.2 Intended policy](assets/docs/RBAC_REFERENCE.md#42-intended-policy-target-behavior-align-crud-with-this)
|
|
||||||
- [5. ACME, accounts, cache, and agent](assets/docs/RBAC_REFERENCE.md#5-acme-accounts-cache-and-agent)
|
|
||||||
- [6. Managing API keys](assets/docs/RBAC_REFERENCE.md#6-managing-api-keys)
|
|
||||||
- [7. Calling the API with an API key](assets/docs/RBAC_REFERENCE.md#7-calling-the-api-with-an-api-key)
|
|
||||||
- [8. Comparison with MaksIT.Vault](assets/docs/RBAC_REFERENCE.md#8-comparison-with-maksitvault)
|
|
||||||
|
|
||||||
## Patch and delta reference
|
|
||||||
|
|
||||||
How PATCH payloads (deltas) are built and applied is documented in **[assets/docs/PATCH_DELTA_REFERENCE.md](assets/docs/PATCH_DELTA_REFERENCE.md)**. It matches the **MaksIT.Core** contract; this repo focuses on **account** PATCH and **`hostnames`** in the WebUI.
|
|
||||||
|
|
||||||
- [TL;DR (start here)](assets/docs/PATCH_DELTA_REFERENCE.md#tldr-start-here)
|
|
||||||
- [1. Core contract (MaksIT.Core)](assets/docs/PATCH_DELTA_REFERENCE.md#1-core-contract-maksitcore)
|
|
||||||
- [2. Backend (BE) rules](assets/docs/PATCH_DELTA_REFERENCE.md#2-backend-be-rules)
|
|
||||||
- [3. Frontend (FE) rules](assets/docs/PATCH_DELTA_REFERENCE.md#3-frontend-fe-rules)
|
|
||||||
- [4. Payload examples](assets/docs/PATCH_DELTA_REFERENCE.md#4-payload-examples)
|
|
||||||
- [5. Quick reference](assets/docs/PATCH_DELTA_REFERENCE.md#5-quick-reference)
|
|
||||||
- [6. Related docs](assets/docs/PATCH_DELTA_REFERENCE.md#6-related-docs)
|
|
||||||
- [7. Current implementation vs reference](assets/docs/PATCH_DELTA_REFERENCE.md#7-current-implementation-vs-reference-maksit-certsui)
|
|
||||||
|
|
||||||
## Login and refresh token architecture
|
|
||||||
|
|
||||||
How login, JWT access tokens, refresh tokens, axios interceptors, and logout interact is documented in **[assets/docs/LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md](assets/docs/LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md)**. **Certs WebAPI** persists users in PostgreSQL; **2FA** follows whatever this repo’s backend and WebUI implement (shared models may carry optional fields).
|
|
||||||
|
|
||||||
- [1. Overview](assets/docs/LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md#1-overview)
|
|
||||||
- [2. Token model](assets/docs/LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md#2-token-model)
|
|
||||||
- [3. Backend flow](assets/docs/LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md#3-backend-flow)
|
|
||||||
- [4. Frontend flow](assets/docs/LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md#4-frontend-flow)
|
|
||||||
- [5. API summary](assets/docs/LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md#5-api-summary)
|
|
||||||
- [6. Sequence overview](assets/docs/LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md#6-sequence-overview)
|
|
||||||
- [7. Security notes](assets/docs/LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md#7-security-notes)
|
|
||||||
- [8. Key files reference](assets/docs/LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md#8-key-files-reference)
|
|
||||||
|
|
||||||
## Reverse proxy routing (YARP)
|
|
||||||
|
|
||||||
How the **YARP** edge splits **ACME challenge**, **Swagger**, **WebAPI**, and **WebUI** traffic is documented in **[assets/docs/REVERSE_PROXY_ROUTING.md](assets/docs/REVERSE_PROXY_ROUTING.md)**, including **`/.well-known/acme-challenge/`** for HTTP-01.
|
|
||||||
|
|
||||||
- [Route table](assets/docs/REVERSE_PROXY_ROUTING.md#route-table)
|
|
||||||
- [HTTP-01 (Let’s Encrypt)](assets/docs/REVERSE_PROXY_ROUTING.md#http-01-lets-encrypt)
|
|
||||||
- [Kubernetes (Helm)](assets/docs/REVERSE_PROXY_ROUTING.md#kubernetes-helm)
|
|
||||||
- [Automation and clients](assets/docs/REVERSE_PROXY_ROUTING.md#automation-and-clients)
|
|
||||||
- [Direct vs proxied ports (local dev)](assets/docs/REVERSE_PROXY_ROUTING.md#direct-vs-proxied-ports-local-dev)
|
|
||||||
- [Related files](assets/docs/REVERSE_PROXY_ROUTING.md#related-files)
|
|
||||||
|
|
||||||
## High availability architecture
|
|
||||||
|
|
||||||
High-availability behavior for ACME coordination, challenge coherence, leases, background services, and Kubernetes probes is documented in **[assets/docs/HA_ARCHITECTURE.md](assets/docs/HA_ARCHITECTURE.md)**.
|
|
||||||
|
|
||||||
- [Goals and runtime model](assets/docs/HA_ARCHITECTURE.md#goals)
|
|
||||||
- [Lease design](assets/docs/HA_ARCHITECTURE.md#lease-design)
|
|
||||||
- [HTTP-01 coherence design](assets/docs/HA_ARCHITECTURE.md#http-01-coherence-design)
|
|
||||||
- [Kubernetes behavior](assets/docs/HA_ARCHITECTURE.md#kubernetes-behavior)
|
|
||||||
- [Files involved](assets/docs/HA_ARCHITECTURE.md#files-involved)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -723,7 +656,7 @@ The Helm chart in [`src/helm`](src/helm) deploys **server**, **client**, and **r
|
|||||||
3. **Verify** that pods in the `certs-ui` namespace can reach the database host and port (DNS, network policies, TLS/`SslMode` as required).
|
3. **Verify** that pods in the `certs-ui` namespace can reach the database host and port (DNS, network policies, TLS/`SslMode` as required).
|
||||||
4. **Configure** `certsServerSecrets.certsEngineConfiguration.connectionString` in your values overlay or Secret (see [step 2](#2-prepare-namespace-secrets-and-configmap) and [`src/helm/values.yaml`](src/helm/values.yaml)). The chart default is an empty placeholder until you set it.
|
4. **Configure** `certsServerSecrets.certsEngineConfiguration.connectionString` in your values overlay or Secret (see [step 2](#2-prepare-namespace-secrets-and-configmap) and [`src/helm/values.yaml`](src/helm/values.yaml)). The chart default is an empty placeholder until you set it.
|
||||||
|
|
||||||
For **high availability** (`components.server.replicaCount` > 1), use a **shared** PostgreSQL deployment that every server replica can reach. The application stores users, refresh tokens, ACME sessions, HTTP-01 challenge tokens, and runtime leases in PostgreSQL—not on server PVCs. See [High availability architecture](#high-availability-architecture) and [`assets/docs/HA_ARCHITECTURE.md`](assets/docs/HA_ARCHITECTURE.md). A single PostgreSQL instance is acceptable for development or single-replica clusters if it meets your availability and backup needs.
|
For **high availability** (`components.server.replicaCount` > 1), use a **shared** PostgreSQL deployment that every server replica can reach. The application stores users, refresh tokens, ACME sessions, HTTP-01 challenge tokens, and runtime leases in PostgreSQL—not on server PVCs. Server replicas are **symmetric** (no elected primary); short-lived Postgres leases (`certs-ui-bootstrap`, `certs-ui-renewal-sweep`) coordinate bootstrap and renewal sweeps — see Helm **`NOTES.txt`** after install and [Health endpoints](#architecture-and-documentation) above. A single PostgreSQL instance is acceptable for development or single-replica clusters if it meets your availability and backup needs.
|
||||||
|
|
||||||
Unlike Docker/Podman Compose in this repo (which includes a `postgres` service in `docker-compose`), the Kubernetes chart expects you to operate the database separately.
|
Unlike Docker/Podman Compose in this repo (which includes a `postgres` service in `docker-compose`), the Kubernetes chart expects you to operate the database separately.
|
||||||
|
|
||||||
|
|||||||
@ -1,358 +0,0 @@
|
|||||||
# Architecture layering (Certs UI)
|
|
||||||
|
|
||||||
How **MaksIT.CertsUI** (host) and **MaksIT.CertsUI.Engine** (library) split work so HTTP, business rules, and PostgreSQL stay in the right place. Complements `assets/docs/` (HA, auth, proxy).
|
|
||||||
|
|
||||||
**Branches, PRs, changelog:** [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## At a glance
|
|
||||||
|
|
||||||
| | **Host** `MaksIT.CertsUI` | **Engine** `MaksIT.CertsUI.Engine` |
|
|
||||||
|--|---------------------------|-------------------------------------|
|
|
||||||
| **Owns** | Controllers, app `Services/`, auth, DI, `ToActionResult()`, ProblemDetails | `Domain/`, `DomainServices/`, `Persistence/`, `QueryServices/`, integration `Services/` (e.g. ACME HTTP) |
|
|
||||||
| **Must not** | Linq2Db, raw SQL, `IPersistenceService` / `IQueryService` in controllers | `IActionResult`, HTTP types, host-only policy |
|
|
||||||
| **Returns** | HTTP responses | `Result` / `Result<T>` (`MaksIT.Results`) |
|
|
||||||
|
|
||||||
**Single spine (request direction):**
|
|
||||||
|
|
||||||
```text
|
|
||||||
Controller → App Service → IDomainService → IPersistenceService OR IQueryService → Linq2Db → PostgreSQL
|
|
||||||
```
|
|
||||||
|
|
||||||
**Shortcut (thin paged search in this repo):** **Controller → App Service → `I*QueryService` → …** with **no** **`IDomainService`** hop—see [Pattern B](#pattern-b-thin-search) (`IdentityService.SearchUsersAsync`, `ApiKeyService.Search…`).
|
|
||||||
|
|
||||||
**App Service** is drawn as a **box that contains** **`Mappers/`** on both sides of the Engine call: **Request / wire models → domain or engine inputs** (outbound), then **engine `Result` / `Query/` / domain → Response DTOs** (inbound). Mapping types live under **`MaksIT.CertsUI/Mappers/`**; orchestration and **`Result`** handling live in **`Services/`**.
|
|
||||||
|
|
||||||
Details: **[Request flow](#request-flow-to-database)** → **[Response flow](#response-flow-to-client)**. Reads: **[Query flow](#query-flow-reads)** (full stack vs thin search).
|
|
||||||
|
|
||||||
ACME HTTP and similar integration run **inside** the `IDomainService` step—not a parallel stack.
|
|
||||||
|
|
||||||
**Same layering as sibling MaksIT apps:** thin host `Services/` call **`I*DomainService`** (or **`I*QueryService`** for thin search). Hosted jobs resolve services via **`IServiceScope`** / **`IServiceScopeFactory`** and **Engine `IDomainService`** (and may call a **thin host façade** such as **`ICertsFlowService`**, which forwards to **`ICertsFlowDomainService`**); they **do not** resolve **`IPersistenceService`** / **`IQueryService`** directly from the worker class.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Diagram convention:** **Every** spine figure is **linear**: a **single** **`-->`** chain (**no forks**). **Outbound** uses **`flowchart TB`** (**1** at top → **8 · PostgreSQL** at bottom). **Inbound** (response) uses **`flowchart BT`** (**8 → … → 1** toward HTTP). **Step 3 · App Service** appears as **one or more consecutive nodes** on that chain (labels name the beats).
|
|
||||||
|
|
||||||
## Request flow (to database)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
%% Spine outbound: request 1 to 8
|
|
||||||
flowchart TB
|
|
||||||
o1["1 Controller"] --> o2["2 Request"] --> o_map["3 · Mappers: Request to domain or engine inputs"] --> o_orch["3 · Orchestration, call IDomainService"] --> o5["5 DomainService"] --> o6["6 Persist or Query port"] --> o7["7 Linq2Db + Dto"] --> o8[(8 PostgreSQL)]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Step | Where | Role |
|
|
||||||
|:----:|-------|------|
|
|
||||||
| 1 | Host | Call **one** app service; `Result` → `ToActionResult()` (or `Content` for ACME token). |
|
|
||||||
| 2 | Host | Route + `MaksIT.Models` (or host models). |
|
|
||||||
| 3 | Host | **`App Service`**: **`Mappers/`** · Request → domain / engine inputs; orchestration; then **`IDomainService`** *or* (thin search) **`I*QueryService`**—see **Response** for **`Mappers/`** → Response DTOs. **`CertsUIAuthorizationFilter`** (JWT or **`X-API-KEY`**) runs on the **controller**; RBAC and tenancy rules live in app **`Services/`** — see [USER_AND_API_KEY_RBAC.md](./USER_AND_API_KEY_RBAC.md). |
|
|
||||||
| 5 | Engine | **`IDomainService`**: rules + orchestration; may call Engine **`Services/`** (HTTP). |
|
|
||||||
| 6 | Engine | **`IPersistenceService`** *or* **`IQueryService`** (one style per hop). |
|
|
||||||
| 7 | Engine | Linq2Db + **`Dto/`**; **persist mappers** for row / JSON columns. |
|
|
||||||
| 8 | DB | PostgreSQL. |
|
|
||||||
|
|
||||||
**Shortcuts (still one line, not a fork):**
|
|
||||||
|
|
||||||
- **Search / list:** sometimes **3 → 6** with no extra domain logic: app service → **`IQueryService`** → Linq2Db (middle steps **skipped**) — see **[Query flow — Pattern B](#pattern-b-thin-search)**.
|
|
||||||
- **Hosted:** **`IServiceScopeFactory`** → e.g. **`IRegistrationCacheDomainService`**, **`ICertsFlowDomainService`**, **`ICertsFlowService`** (`AutoRenewal`); never inject **`IPersistenceService`** / **`IQueryService`** on the **hosted** class itself.
|
|
||||||
|
|
||||||
**Variants at steps 6–7 only:**
|
|
||||||
|
|
||||||
| | Step 6 | Step 7 |
|
|
||||||
|--|--------|--------|
|
|
||||||
| **Write** | `IPersistenceService` | Domain→Dto if needed, then write |
|
|
||||||
| **Read by key** | `IPersistenceService` | Read Dto, Dto→Domain, return |
|
|
||||||
| **Read search** | `IQueryService` | Projection → domain or `Query/` type |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Response flow (to client)
|
|
||||||
|
|
||||||
Data and **`Result<T>`** unwind **stage by stage** toward HTTP. Nothing “teleports” from Linq2Db to the controller: each layer maps what it owns.
|
|
||||||
|
|
||||||
**Spine (database → wire):** **data and `Result<T>`** walk **8→1** (PostgreSQL → … → Controller) in **one line**. Steps **8–5** are Engine-side (same as outbound). **3 · App Service** is **three beats on that line**: from Engine → unwrap **`Result`** → orchestrate → **`Mappers/`** · domain / **`Query/`** → **`MaksIT.Models`** / response DTOs. The Mermaid figure uses **`flowchart BT`** so arrows run **toward HTTP** (not toward the DB).
|
|
||||||
|
|
||||||
```text
|
|
||||||
PostgreSQL → … → IDomainService → App Service (internal: Mappers · → Response DTOs) → Controller → ToActionResult() / Content / ProblemDetails
|
|
||||||
```
|
|
||||||
|
|
||||||
| Step | Direction | Responsibility |
|
|
||||||
|:----:|-------------|----------------|
|
|
||||||
| 8→7 | Engine | **Persist path:** materialize **`Dto/`**; **`Persistence/Mappers`**: **Dto → domain**. **Query path:** materialize **`Dto/`** (or joined Dtos); **`QueryServices/.../Linq2Db`**: **Dto → `Query/`** (e.g. `MapToQueryResult`). |
|
|
||||||
| 7→6 | Engine | Port returns domain, **`Query/`** read model, or **`Result<T>`** payload. |
|
|
||||||
| 6→5 | Engine | **`IDomainService`** may enrich, validate, or aggregate before returning **`Result`**. |
|
|
||||||
| 5→3 | Host | **`App Service`**: orchestration · unwrap **`Result`**; **`MaksIT.CertsUI/Mappers`** · engine outputs / **`Query/`** / domain → response DTOs (**three consecutive nodes for step 3** in diagram). |
|
|
||||||
| 3→1 | Host | **`Controller`**: **`ToActionResult(result)`** or **`Content(...)`** (ACME). |
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
%% Spine inbound: response 8 to 1 toward HTTP
|
|
||||||
flowchart BT
|
|
||||||
i8[(8 PostgreSQL)] --> i7["7 Linq2Db + Dto"] --> i6["6 Persist or Query port"] --> i5["5 DomainService"] --> i_from["3 · From Engine: Result, domain, Query"] --> i_orch["3 · Orchestration, unwrap Result"] --> i_map["3 · Mappers to Response DTOs"] --> i2["2 Request binding context"] --> i1["1 Controller ToActionResult or Content"]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Thin search shortcut:** if the request skipped **`IDomainService`**, the response still walks **7 → 6 → (skip 5) → 3 → 1**: **`Query/`** (already mapped from **`Dto/`** in step 7) → **`App Service`** (mapper inside step **3**) → **`Result`** → **`ToActionResult()`**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Query flow (reads)
|
|
||||||
|
|
||||||
Reads always hit **PostgreSQL through Linq2Db**; only **who calls the read port** changes.
|
|
||||||
|
|
||||||
**Mapping to query results (Engine read model):** inside **`QueryServices/.../Linq2Db`**, Linq2Db materializes **`Dto/`** table rows (or joins). Implementations then **map `Dto` → types under `Query/`** (e.g. `UserQueryResult`, `ApiKeyQueryResult`) before returning **`Result<List<…>>`** from **`Search`**. That **Dto → `Query/`** step is the **query-side read mapper**—not web API mappers and not **`Persistence/Mappers`** (those are for writes / JSON columns / domain load). See e.g. **`UserQueryServiceLinq2Db`** (`MapToQueryResult`).
|
|
||||||
|
|
||||||
**Predicates:** **`IUserQueryService`** and **`IApiKeyQueryService`** take **`Expression<Func<TDto, bool>>?`** plus **`skip` / `limit`** and a separate **`Count`** with the same predicate. The **host** builds translatable predicates (today: simple filters such as **`Contains`** on username/description). **`ExpressionCompose`** (`QueryServices/ExpressionCompose.cs`) is available when you need composed predicates through navigation—not required for the current search callers.
|
|
||||||
|
|
||||||
**Inside `IQueryService` (Linq2Db implementation):**
|
|
||||||
|
|
||||||
```text
|
|
||||||
PostgreSQL → Linq2Db (materialize Dto / joins) → optional Where(predicate) → map Dto → Query/ types → Result<List<…>> to caller; Count uses the same predicate
|
|
||||||
```
|
|
||||||
|
|
||||||
The caller is either **`IDomainService`** (Pattern A) or **app `Service`** (Pattern B).
|
|
||||||
|
|
||||||
### Pattern A: Domain-centered read (`IDomainService`)
|
|
||||||
|
|
||||||
Use when a use case must go through **one Engine orchestration place** (invariants, multiple ports, ACME side effects, load-by-key).
|
|
||||||
|
|
||||||
**In this repo, Identity and API keys use persistence for domain loads, not the query port:** e.g. **`ReadUserByIdAsync`** and **`ReadAPIKeyAsync`** go **App → `I*DomainService` → `I*PersistenceService` → Linq2Db** (**Dto → domain** via **`Persistence/Mappers`**), not **`I*QueryService`**.
|
|
||||||
|
|
||||||
**When listing is owned by the domain** (not implemented for Identity/API key search here), the shape is:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Controller → App Service → IDomainService → IQueryService (PostgreSQL → … → Query/) → Result back up
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:** if the domain sits in the middle, unwind through **`IDomainService`**; if the request used **Pattern B**, **`IDomainService`** is skipped on the return path too (see [Response flow](#response-flow-to-client)).
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
%% Query Pattern A outbound
|
|
||||||
flowchart TB
|
|
||||||
a1["1 Controller"] --> a2["2 Request"] --> a_map["3 · Mappers: Request to domain or engine inputs"] --> a_orch["3 · Orchestration, call IDomainService"] --> a5["5 DomainService"] --> a6["6 Persist or Query port"] --> a7["7 Linq2Db + Dto"] --> a8[(8 PostgreSQL)]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pattern B: Thin search
|
|
||||||
|
|
||||||
Use when **only filtering + paging + projection** are needed and **no extra engine rules** apply. **This repo:** **`IdentityService.SearchUsers`** and **`ApiKeyService.Search…`** (`GetActingJwtTokenData` on the **controller**; RBAC predicate built in the app service).
|
|
||||||
|
|
||||||
```text
|
|
||||||
Controller → App Service → IQueryService (impl: PostgreSQL → Linq2Db Dto → map → Query/) → Result back up
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:** **`IQueryService`** returns **`Result<List<QueryType>?>`** from **`Search`** (rows already mapped from **`Dto/`**); **`Count`** supplies **`TotalRecords`**. **App service** maps **`Query/`** → **`MaksIT.Models`** / paged API DTOs → controller → **`ToActionResult()`**.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
%% Query Pattern B outbound thin search
|
|
||||||
flowchart TB
|
|
||||||
b1["1 Controller"] --> b2["2 Request"] --> b_map["3 · Mappers · predicates"] --> b_qry["3 · IQueryService"] -->|"skip DomainService"| b6["6 Persist or Query port"] --> b7["7 Linq2Db + Dto"] --> b8[(8 PostgreSQL)]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pick A vs B:** **B** is what **Identity** and **API key** **search** use today. Prefer **A** (domain calls **`IQueryService`**) when list rules must live in Engine; **get-by-id** here stays **domain → `IPersistenceService`**, not **`IQueryService`**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Layers (detail)
|
|
||||||
|
|
||||||
### Host — `MaksIT.CertsUI`
|
|
||||||
|
|
||||||
| Layer | Responsibility |
|
|
||||||
|-------|----------------|
|
|
||||||
| **Controllers** | Thin: app service + `ToActionResult()`. No business rules, no Linq2Db. |
|
|
||||||
| **Models** | Often **`MaksIT.Models`** for shared API shapes with **MaksIT.WebUI**. |
|
|
||||||
| **App `Services/`** | **Only** controller entry for a use case: orchestration · **`IDomainService`** / **`IQueryService`**; **invokes** **`Mappers/`** before Engine (**Request** → domain / engine inputs) and after Engine (**`Result`** / **`Query/`** / domain → **Response** DTOs). |
|
|
||||||
| **Web `Mappers/`** (`Mappers/`) | Types used **from** app services: **Request** → domain / engine inputs; engine outputs → **Response** DTOs. Not **`Engine/Persistence/Mappers`** (table / JSON payloads). |
|
|
||||||
|
|
||||||
### Engine — `MaksIT.CertsUI.Engine`
|
|
||||||
|
|
||||||
| Layer | Responsibility |
|
|
||||||
|-------|----------------|
|
|
||||||
| **`DomainServices/`** | Use cases: **`IPersistenceService`**, Engine **`Services/`**; may also call **`IQueryService`** when a use case should own search (Identity/API key **search** in this repo is **Pattern B** from the host). No HTTP return types. |
|
|
||||||
| **`Persistence/`** | Writes (and load-by-key APIs): **`I*PersistenceService`** + Linq2Db, **`Dto/`**, **`Persistence/Mappers`**. |
|
|
||||||
| **`QueryServices/`** | Reads: **`I*QueryService`** + Linq2Db + **`Query/`**. |
|
|
||||||
| **`Services/`** (Engine) | Integration **used by** `DomainServices` (e.g. **`ILetsEncryptService`**). Not a second app-service layer. |
|
|
||||||
| **`Domain/`** | Entities / value objects: no Linq2Db, HTTP, or host types. |
|
|
||||||
|
|
||||||
**Engine slice (same idea as the spine):**
|
|
||||||
|
|
||||||
| | Purpose | Examples |
|
|
||||||
|--|---------|----------|
|
|
||||||
| **DomainServices** | Orchestrate persistence + integration; **`IQueryService`** only when domain owns search (not Identity/API key search here) | `CertsFlowDomainService`, `IdentityDomainService`, `RegistrationCacheDomainService` |
|
|
||||||
| **QueryServices** | Read port | `IUserQueryService` + `UserQueryServiceLinq2Db` |
|
|
||||||
| **Persistence** | Write port (+ loads exposed as persistence API) | `IRegistrationCachePersistenceService` |
|
|
||||||
| **Services** | Outbound HTTP / protocol | `ILetsEncryptService` |
|
|
||||||
|
|
||||||
**Guideline:** **`GetTable<>` / SQL** only in **`.../Linq2Db`** under Persistence and QueryServices.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Solution map
|
|
||||||
|
|
||||||
| Project | Role |
|
|
||||||
|---------|------|
|
|
||||||
| **MaksIT.CertsUI** | ASP.NET: controllers, app services, mappers, DI, pipeline. |
|
|
||||||
| **MaksIT.CertsUI.Engine** | Domain, domain services, persistence, queries, migrations, ACME integration. |
|
|
||||||
| **MaksIT.Models** | Shared request/response for WebUI + API. |
|
|
||||||
| **MaksIT.WebUI** | React SPA. |
|
|
||||||
| **ReverseProxy** | Optional YARP. |
|
|
||||||
| **\*Tests** | Unit (Engine) and integration (host + DB). |
|
|
||||||
|
|
||||||
**Golden rule:** HTTP and status codes stay in the **host**; PostgreSQL and reusable orchestration stay in the **Engine** behind **`DomainServices`** (and persist/query ports).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Folder layout
|
|
||||||
|
|
||||||
### `src/MaksIT.CertsUI/`
|
|
||||||
|
|
||||||
Host-only: composition, HTTP, auth, thin façades into Engine.
|
|
||||||
|
|
||||||
```
|
|
||||||
MaksIT.CertsUI/
|
|
||||||
├── Program.cs # Pipeline, DI (AddCertsEngine, filters, hosted services)
|
|
||||||
├── Configuration.cs # IOptions settings + secrets
|
|
||||||
├── Controllers/ # Thin; Result → ToActionResult (or Content for ACME)
|
|
||||||
├── Services/ # App services → IDomainService / IQueryService (search)
|
|
||||||
├── Mappers/ # Request/Response ↔ domain or Query/ (not Engine persist mappers)
|
|
||||||
├── Authorization/ # JWT, API key, filters, HttpContext helpers
|
|
||||||
├── Abstractions/ # e.g. ServiceBase, BaseAsyncAuthorizationFilter
|
|
||||||
├── HostedServices/ # Scope + engine domain (+ thin ICertsFlowService, etc.); not IPersistenceService on the worker
|
|
||||||
├── Infrastructure/ # e.g. IRuntimeInstanceIdProvider for HA
|
|
||||||
└── Properties/
|
|
||||||
```
|
|
||||||
|
|
||||||
*(Omitted from tree: `bin/`, `obj/`, `appsettings*.json`, Docker.)*
|
|
||||||
|
|
||||||
### `src/MaksIT.CertsUI.Engine/` (`Engine/` in prose)
|
|
||||||
|
|
||||||
```
|
|
||||||
Engine/
|
|
||||||
├── Domain/
|
|
||||||
├── DomainServices/
|
|
||||||
├── Dto/
|
|
||||||
├── Data/ # CertsLinq2DbMapping
|
|
||||||
├── Persistence/
|
|
||||||
│ ├── Mappers/
|
|
||||||
│ └── Services/ … Linq2Db/
|
|
||||||
├── Query/
|
|
||||||
├── QueryServices/ # I*QueryService + ExpressionCompose; Linq2Db/… implementations
|
|
||||||
├── Services/ # LetsEncrypt etc. — called from DomainServices
|
|
||||||
├── Infrastructure/
|
|
||||||
├── FluentMigrations/
|
|
||||||
├── RuntimeCoordination/ # HA — see HA_ARCHITECTURE.md
|
|
||||||
├── Extensions/ # AddCertsEngine
|
|
||||||
└── Facades/ …
|
|
||||||
```
|
|
||||||
|
|
||||||
**Spelling:** most paths use **`Persistence`** (historic). A few types say **`Persistence`** — keep per-file consistency; do not mass-rename in small PRs.
|
|
||||||
|
|
||||||
**Mapper kinds:**
|
|
||||||
|
|
||||||
| Where | Maps |
|
|
||||||
|-------|------|
|
|
||||||
| **`MaksIT.CertsUI/Mappers`** | API request/response ↔ domain or **`Query/`** |
|
|
||||||
| **`Engine/Persistence/Mappers`** | Domain ↔ table row / JSON column (`Dto`) |
|
|
||||||
| **`Engine/QueryServices/.../Linq2Db`** (inline or private helpers) | **`Dto/`** rows / joins → **`Query/`** read models (e.g. `UserQueryResult`) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## As-built routes (quick map)
|
|
||||||
|
|
||||||
Each row fits the **spine** above; only **persist vs query** and **skipped** steps differ.
|
|
||||||
|
|
||||||
### API + Engine
|
|
||||||
|
|
||||||
| Area | Controller | App service | Engine |
|
|
||||||
|------|------------|-------------|--------|
|
|
||||||
| ACME steps | `CertsFlowController` | `CertsFlowService` | `ICertsFlowDomainService` |
|
|
||||||
| ACME token | `WellKnownController` | `CertsFlowService` | `ICertsFlowDomainService` → `Content(text/plain)` |
|
|
||||||
| Identity CRUD / login | `IdentityController` | `IdentityService` | `IIdentityDomainService` |
|
|
||||||
| Identity search | `IdentityController` | `IdentityService` | `IUserQueryService` → mapper |
|
|
||||||
| API keys | `APIKeyController` | `ApiKeyService` | `IApiKeyDomainService` / `IApiKeyQueryService` |
|
|
||||||
| Registration cache | `CacheController` | `CacheService` | `IRegistrationCacheDomainService` |
|
|
||||||
| Accounts / certs | `AccountController` | `AccountService` | `ICertsFlowService` → `ICertsFlowDomainService` |
|
|
||||||
|
|
||||||
### Authorization (filters)
|
|
||||||
|
|
||||||
| Route prefix | Filter | Principal |
|
|
||||||
|--------------|--------|-----------|
|
|
||||||
| `/api/identity/...` (except login, refresh) | `CertsUIAuthorizationFilter` | JWT **or** `X-API-KEY` → `GetActingJwtTokenData()` on admin actions |
|
|
||||||
| `/api/apikey/...` | `CertsUIAuthorizationFilter` | JWT **or** `X-API-KEY` |
|
|
||||||
| `/api/account/...`, `/api/certs/...`, `/api/cache/...`, `/api/agent/...`, `/api/debug/...` | `CertsUIAuthorizationFilter` | JWT **or** `X-API-KEY` (`CertsUIAuthorizationData`) |
|
|
||||||
| `/.well-known/acme-challenge/...` | *(none)* | Public HTTP-01 |
|
|
||||||
|
|
||||||
See [USER_AND_API_KEY_RBAC.md](./USER_AND_API_KEY_RBAC.md) and [RBAC_REFERENCE.md](./RBAC_REFERENCE.md).
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
| Area | Entry | Notes |
|
|
||||||
|------|-------|------|
|
|
||||||
| Renewal | `AutoRenewal` | Scoped `IRegistrationCacheDomainService`, `ICertsFlowDomainService`, `ICertsFlowService` (calls `FullFlow` / cache load) |
|
|
||||||
| Agent | `AgentController` → `AgentService` | Outbound HTTP only; no Engine domain |
|
|
||||||
| Debug | `DebugController` | `IRuntimeInstanceId`; bypasses app services |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Hard rules
|
|
||||||
|
|
||||||
1. **Controllers** → app **`Services/`** only (except trivial debug). No **`IPersistenceService`**, **`IQueryService`**, **`ILetsEncryptService`** on controllers.
|
|
||||||
2. **App services** → **`IDomainService`** for use cases; **`CacheService`** → **`IRegistrationCacheDomainService`**. Search may use **`I*QueryService`** (see Identity / API keys).
|
|
||||||
3. **`Domain/`** → no Persistence, Linq2Db, `HttpClient`, host types.
|
|
||||||
4. **`DomainServices/`** → ports + Engine **`Services/`**; no raw SQL / `GetTable<>` (only in Linq2Db types).
|
|
||||||
5. **Persistence Linq2Db** → **`ICertsDataConnectionFactory`**, **`Dto/`**, **`Data/CertsLinq2DbMapping`**.
|
|
||||||
6. **Engine JSON** (DB / zip) → **`MaksIT.Core`** `ToJson()` / `ToObject<T>()` (STJ); no new Newtonsoft in Engine.
|
|
||||||
7. **HTTP status** → host `ToActionResult()`; Engine stays on **`Result`**.
|
|
||||||
|
|
||||||
**Variance:** “DomainService on every read” is not required for **thin paged search**; adding a domain wrapper is optional if shared rules appear.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Host details
|
|
||||||
|
|
||||||
- **Controllers:** inject **`Services/*`**; **`WellKnownController`** uses **`Content(..., "text/plain")`** for ACME.
|
|
||||||
- **App services:** inject Engine ports (**`IDomainService`**, **`IQueryService`** when searching), **`IOptions`**, other app services; **call `Mappers/`** for **Request → engine-shaped inputs** and **`Result` / `Query/` / domain → API response models**—keep mapping logic in **`MaksIT.CertsUI/Mappers/`**, orchestration in **`Services/`**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dependency injection (Engine)
|
|
||||||
|
|
||||||
Central: **`Extensions/ServiceCollectionExtensions.cs`** (`AddCertsEngine`).
|
|
||||||
|
|
||||||
- **`ICertsDataConnectionFactory`** — Scoped
|
|
||||||
- **Persistence / query Linq2Db** — Scoped
|
|
||||||
- **`ILetsEncryptService`** — typed `HttpClient`
|
|
||||||
- **`IRegistrationCacheDomainService`** — Scoped
|
|
||||||
- **`IRuntimeLeaseService`** — Singleton where HA docs say so
|
|
||||||
|
|
||||||
Avoid **Scoped** inside **Singleton** without `IServiceScopeFactory`; use scoped persistence for per-request work.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
- **MaksIT.CertsUI.Engine.Tests** — Engine unit tests (no full host).
|
|
||||||
- **MaksIT.CertsUI.Tests** — Integration tests + PostgreSQL; mirror production DI where possible.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contributor checklist
|
|
||||||
|
|
||||||
1. New **HTTP use case** → controller → app **`Services/`** → **`IDomainService`** (or **`IQueryService`** for thin paging only).
|
|
||||||
2. New **DB write** → **`Persistence/Services/I…`** + **`Linq2Db/`** + register; **`DomainServices`** call persistence—not host **`CacheService`** for engine rules.
|
|
||||||
3. New **read/report** → **`QueryServices`** + **`Query/`**; either domain service or app service calls query—**match** Identity / API key search.
|
|
||||||
4. New **table shape** → **`Dto/`** + **`CertsLinq2DbMapping`**.
|
|
||||||
5. **JSON column mapping** → **`Persistence/Mappers`**, not controllers.
|
|
||||||
6. **API contract** → **`MaksIT.Models`** + web mappers, not Engine `Dto` unless it is a real table shape.
|
|
||||||
7. **Invariants** → **`Domain/`** or **`DomainServices/`**.
|
|
||||||
8. **OpenAPI / status codes** → host only.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Related docs
|
|
||||||
|
|
||||||
- [USER_AND_API_KEY_RBAC.md](./USER_AND_API_KEY_RBAC.md)
|
|
||||||
- [RBAC_REFERENCE.md](./RBAC_REFERENCE.md)
|
|
||||||
- [HA_ARCHITECTURE.md](./HA_ARCHITECTURE.md)
|
|
||||||
- [LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md](./LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md)
|
|
||||||
- [REVERSE_PROXY_ROUTING.md](./REVERSE_PROXY_ROUTING.md)
|
|
||||||
- [PATCH_DELTA_REFERENCE.md](./PATCH_DELTA_REFERENCE.md)
|
|
||||||
- [POWERSHELL_CLIENT_MODULE.md](./POWERSHELL_CLIENT_MODULE.md)
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
# High Availability Architecture
|
|
||||||
|
|
||||||
This document explains how HA works in `MaksIT.CertsUI` after moving mutable ACME coordination state to PostgreSQL.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
- Run multiple `server` replicas without ACME race conditions.
|
|
||||||
- Keep HTTP-01 challenge tokens coherent across replicas.
|
|
||||||
- Ensure startup/bootstrap and renewal loops do not run in parallel on every pod.
|
|
||||||
- Expose health endpoints suitable for Kubernetes probes.
|
|
||||||
|
|
||||||
## Runtime model
|
|
||||||
|
|
||||||
- **Shared source of truth:** PostgreSQL stores ACME sessions, challenge rows, ToS cache, registration caches, and runtime leases.
|
|
||||||
- **Per-instance identity:** each running server process gets one canonical `InstanceId` (`IRuntimeInstanceId` singleton).
|
|
||||||
- **Lease holder:** `NewOrderAsync` acquires **AcmeWriter**; startup uses **BootstrapCoordinator**; each renewal sweep uses **RenewalSweep** (see `RuntimeLeaseNames`). All leases are rows in **`app_runtime_leases`** with TTL semantics—no long-lived leader object in the app.
|
|
||||||
- **Challenge reads:** `/.well-known/acme-challenge/{token}` returns the token value from PostgreSQL (no local ACME directory).
|
|
||||||
- **Background coordination:** bootstrap and renewal hosted services use named leases to avoid duplicate work.
|
|
||||||
|
|
||||||
## Lease design
|
|
||||||
|
|
||||||
- Lease table key: `lease_name`.
|
|
||||||
- Lease owner: `holder_id` (instance id).
|
|
||||||
- Acquire semantics:
|
|
||||||
- insert new row if missing;
|
|
||||||
- steal only when expired;
|
|
||||||
- renew when current holder matches.
|
|
||||||
- Release semantics:
|
|
||||||
- delete only when `lease_name` and `holder_id` both match.
|
|
||||||
|
|
||||||
This is implemented as an optimistic single-statement `INSERT ... ON CONFLICT ... DO UPDATE ... WHERE ...` flow in PostgreSQL.
|
|
||||||
|
|
||||||
## HTTP-01 coherence design
|
|
||||||
|
|
||||||
- `NewOrderAsync` stores challenge tokens in `acme_http_challenges` via `UpsertAsync`.
|
|
||||||
- Challenge handler (`AcmeChallengeAsync`) reads the token value from the database and returns it as plain text.
|
|
||||||
- Cleanup: auto-renewal loop calls `DeleteOlderThanAsync(TimeSpan.FromDays(10))`.
|
|
||||||
|
|
||||||
## Kubernetes behavior
|
|
||||||
|
|
||||||
- Set `components.server.replicaCount >= 2` with **shared external PostgreSQL** (the Helm chart does not deploy Postgres).
|
|
||||||
- Set **`certsServerSecrets.certsEngineConfiguration.connectionString`**, **`adminUsername`** / **`adminPassword`**, **`jwtSecret`**, and **`passwordPepper`** (or an existing Secret with `appsecrets.json`).
|
|
||||||
- Probes: `GET /health/live` (process up), `GET /health/ready` (PostgreSQL + migrations + bootstrap coordination complete), `GET /health/startup` (JSON phase timings for debugging).
|
|
||||||
- Server pods use a **startupProbe** on `/health/ready` so slow first boot (FluentMigrator, admin bootstrap) does not fail liveness/readiness prematurely.
|
|
||||||
- Helm sets `POD_NAME` from `metadata.name` for stable per-pod identity.
|
|
||||||
- No application-data PVC is required (ACME sessions, HTTP-01 tokens, and identity state live in PostgreSQL).
|
|
||||||
|
|
||||||
## Startup sequence
|
|
||||||
|
|
||||||
1. **PostgreSQL** — accept connections on maintenance DB (`postgres`), then create app database if missing.
|
|
||||||
2. **FluentMigrator** — `MigrateUp` with retries while Postgres is still initializing.
|
|
||||||
3. **Coordination DDL** — `app_runtime_leases`.
|
|
||||||
4. **Schema sync** — optional add-only column sync when `AutoSyncSchema` is enabled.
|
|
||||||
5. **Bootstrap coordination** — one replica acquires the `certs-ui-bootstrap` lease and seeds the global admin; followers wait until an admin exists.
|
|
||||||
|
|
||||||
**Docker Compose (local dev):** bundled `postgres` service with `pg_isready` healthcheck; `server` starts only after `service_healthy`. Connection string comes from mounted `appsecrets.json`, not Helm values.
|
|
||||||
|
|
||||||
Phase timings are tracked in **`CertsStartupState`** and exposed at **`GET /health/startup`**.
|
|
||||||
|
|
||||||
## Current non-goals and boundaries
|
|
||||||
|
|
||||||
- **Agent remains single-instance** by design near edge proxy.
|
|
||||||
- **Only HTTP-01** challenge type is supported currently.
|
|
||||||
- Optional split of ACME worker into a dedicated workload is not implemented yet.
|
|
||||||
|
|
||||||
## Files involved
|
|
||||||
|
|
||||||
### Core coordination contracts
|
|
||||||
|
|
||||||
- `src/MaksIT.CertsUI.Engine/RuntimeCoordination/IRuntimeInstanceId.cs`
|
|
||||||
- `src/MaksIT.CertsUI.Engine/RuntimeCoordination/RuntimeLeaseNames.cs`
|
|
||||||
- `src/MaksIT.CertsUI.Engine/Infrastructure/IRuntimeLeaseService.cs`
|
|
||||||
- `src/MaksIT.CertsUI.Engine/Persistence/Services/IAcmeHttpChallengePersistenceService.cs`
|
|
||||||
|
|
||||||
### PostgreSQL implementation
|
|
||||||
|
|
||||||
- `src/MaksIT.CertsUI.Engine/Infrastructure/RuntimeLeaseServiceNpgsql.cs`
|
|
||||||
- `src/MaksIT.CertsUI.Engine/Persistence/Services/Linq2Db/AcmeHttpChallengePersistenceServiceLinq2Db.cs`
|
|
||||||
- `src/MaksIT.CertsUI.Engine/Data/CertsLinq2DbMapping.cs`
|
|
||||||
- `src/MaksIT.CertsUI.Engine/FluentMigrations/20260425130000_AcmeChallengesAndRuntimeLeases.cs`
|
|
||||||
- `src/MaksIT.CertsUI.Engine/Infrastructure/SchemaSyncService.cs`
|
|
||||||
|
|
||||||
### Startup tracking
|
|
||||||
|
|
||||||
- `src/MaksIT.CertsUI/Infrastructure/CertsStartupState.cs`
|
|
||||||
- `src/MaksIT.CertsUI.Engine/Infrastructure/IDatabaseStartupObserver.cs`
|
|
||||||
- `src/MaksIT.CertsUI.Engine/Infrastructure/DatabaseStartupPhaseRunner.cs`
|
|
||||||
- `src/MaksIT.CertsUI.Engine/Infrastructure/RunMigrationsService.cs`
|
|
||||||
|
|
||||||
### Runtime usage in app flows
|
|
||||||
|
|
||||||
- `src/MaksIT.CertsUI.Engine/DomainServices/CertsFlowDomainService.cs`
|
|
||||||
- `src/MaksIT.CertsUI/HostedServices/InitializationHostedService.cs`
|
|
||||||
- `src/MaksIT.CertsUI/HostedServices/AutoRenewal.cs`
|
|
||||||
- `src/MaksIT.CertsUI/Infrastructure/RuntimeInstanceIdProvider.cs`
|
|
||||||
- `src/MaksIT.CertsUI/Program.cs`
|
|
||||||
- `src/MaksIT.CertsUI/Controllers/WellKnownController.cs`
|
|
||||||
- `src/MaksIT.CertsUI/Services/CertsFlowService.cs`
|
|
||||||
|
|
||||||
### Helm and deployment wiring
|
|
||||||
|
|
||||||
- `src/helm/values.yaml`
|
|
||||||
- `src/helm/templates/deployments.yaml`
|
|
||||||
- `src/helm/templates/poddisruptionbudget.yaml`
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
- `src/MaksIT.CertsUI.Tests/Services/CertsFlowServiceTests.cs`
|
|
||||||
|
|
||||||
## Related docs
|
|
||||||
|
|
||||||
- [ARCHITECTURE_LAYERING.md](./ARCHITECTURE_LAYERING.md) · [USER_AND_API_KEY_RBAC.md](./USER_AND_API_KEY_RBAC.md) · [REVERSE_PROXY_ROUTING.md](./REVERSE_PROXY_ROUTING.md)
|
|
||||||
@ -1,214 +0,0 @@
|
|||||||
# Login and Refresh Token Architecture
|
|
||||||
|
|
||||||
This document describes how authentication (login), token refresh, and logout work across the **MaksIT.CertsUI** WebAPI and WebUI.
|
|
||||||
|
|
||||||
**Audience:** Backend (C# / ASP.NET) and Frontend (TypeScript / React) developers.
|
|
||||||
|
|
||||||
**See also:** [User and API key RBAC](./USER_AND_API_KEY_RBAC.md) — how JWT identity relates to `X-API-KEY` principals, global admin on user vs on key, and `CertsUIAuthorizationFilter` on protected routes.
|
|
||||||
|
|
||||||
**MaksIT.CertsUI** persists users and refresh tokens in **PostgreSQL**. Shared **MaksIT.Models** types may include optional **2FA** fields; behavior follows this WebAPI and WebUI implementation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Overview
|
|
||||||
|
|
||||||
- **Access token:** Short-lived JWT used in the `Authorization: Bearer <token>` header for API calls.
|
|
||||||
- **Refresh token:** Opaque string stored with the user in PostgreSQL; used to obtain a new access token (and optionally a new refresh token) when the access token expires.
|
|
||||||
- **Login** returns both tokens; the client stores them and uses the access token until it expires, then calls **refresh** with the refresh token.
|
|
||||||
- **Logout** revokes the current session (or all sessions) on the server and clears tokens on the client. On Certs, **logout requires** `Authorization: Bearer` with the current access JWT; the server matches that token when removing the session (see §3.4).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Token model
|
|
||||||
|
|
||||||
### 2.1 Backend: `JwtToken` (domain)
|
|
||||||
|
|
||||||
**Location:** `src/MaksIT.CertsUI/Domain/JwtToken.cs`
|
|
||||||
|
|
||||||
| Property | Type | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| `Id` | Guid | Token identifier. |
|
|
||||||
| `Token` | string | The JWT access token string. |
|
|
||||||
| `TokenType` | string | Typically `"Bearer"`. |
|
|
||||||
| `IssuedAt` | DateTime | When the token was issued (UTC). |
|
|
||||||
| `ExpiresAt` | DateTime | When the access token expires. |
|
|
||||||
| `RefreshToken` | string | Opaque refresh token. |
|
|
||||||
| `RefreshTokenExpiresAt` | DateTime | When the refresh token expires. |
|
|
||||||
| `IsRevoked` | bool | If true, token is treated as unusable (revoked entries are removed when resolving refresh). |
|
|
||||||
|
|
||||||
- A **User** holds a list of `JwtToken` instances (multiple devices/sessions).
|
|
||||||
- New tokens are **upserted** via `User.UpsertJwtToken`.
|
|
||||||
|
|
||||||
### 2.2 API response: `LoginResponse`
|
|
||||||
|
|
||||||
**Location:** `src/MaksIT.Models/LetsEncryptServer/Identity/Login/LoginResponse.cs`
|
|
||||||
|
|
||||||
Returned by **login** and **refresh**:
|
|
||||||
|
|
||||||
- `TokenType` (e.g. `"Bearer"`)
|
|
||||||
- `Token` (access JWT)
|
|
||||||
- `ExpiresAt` (access token expiry)
|
|
||||||
- `RefreshToken`
|
|
||||||
- `RefreshTokenExpiresAt`
|
|
||||||
|
|
||||||
There is **no** `Username` field on this model; the WebUI derives display name from **JWT claims** when hydrating identity (`identitySlice`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Backend flow
|
|
||||||
|
|
||||||
### 3.1 Layers
|
|
||||||
|
|
||||||
| Layer | Component | Responsibility |
|
|
||||||
|-------|------------|----------------|
|
|
||||||
| API | `IdentityController` | Login, refresh, logout, and authenticated `PATCH` user. |
|
|
||||||
| Service | `IdentityService` | Validates credentials, issues JWTs via `JwtGenerator`, maps domain `JwtToken` to `LoginResponse`, persists users via `IUserStore`. |
|
|
||||||
| Domain | `User` | Password validation, JWT token list (upsert/remove/revoke). |
|
|
||||||
| Persistence | `IUserStore` | Users and JWT rows persist in PostgreSQL. |
|
|
||||||
|
|
||||||
### 3.2 Login
|
|
||||||
|
|
||||||
**Endpoint:** `POST /api/identity/login`
|
|
||||||
**Controller:** `IdentityController.Login` → `IdentityService.LoginAsync`
|
|
||||||
|
|
||||||
1. **Resolve user** by username (`IUserStore.GetByNameAsync`).
|
|
||||||
2. **Validate password** (`User.ValidatePassword`) with configured **pepper**.
|
|
||||||
3. **Optional 2FA fields** on `LoginRequest` are **not** validated by Certs—ignored if sent.
|
|
||||||
4. **Generate** access JWT via `JwtGenerator.TryGenerateToken` (secret, issuer, audience, expiration from config).
|
|
||||||
5. **Generate** opaque refresh token and build a domain `JwtToken` with access + refresh expiry (`RefreshExpiration` from config).
|
|
||||||
6. **Upsert** token on user, **SetLastLogin**, **`IIdentityPersistenceService.WriteAsync`**.
|
|
||||||
7. Return `LoginResponse` (no username field; client uses JWT claims).
|
|
||||||
|
|
||||||
**Request body (`LoginRequest`):** `username`, `password`, optional unused `twoFactorCode` / `twoFactorRecoveryCode` (shared model shape only).
|
|
||||||
|
|
||||||
### 3.3 Refresh
|
|
||||||
|
|
||||||
**Endpoint:** `POST /api/identity/refresh`
|
|
||||||
**Controller:** `IdentityController.RefreshToken` → `IdentityService.RefreshTokenAsync`
|
|
||||||
|
|
||||||
**Request body (`RefreshTokenRequest`):** `refreshToken` only (`src/MaksIT.Models/LetsEncryptServer/Identity/Login/RefreshTokenRequest.cs`). The WebUI may send a `force` flag for parity with shared thunk code; the Certs API model does **not** include it (extra properties are typically ignored by the serializer).
|
|
||||||
|
|
||||||
1. **Resolve user** by refresh token (`IUserStore.GetByRefreshTokenAsync`).
|
|
||||||
2. **Remove** revoked JWT rows (`RemoveRevokedJwtTokens`).
|
|
||||||
3. **Find** the token where `RefreshToken` matches.
|
|
||||||
4. **Unauthorized** if not found → e.g. “Invalid refresh token.”
|
|
||||||
5. **If the access token is still valid** (`UtcNow <= token.ExpiresAt`): update last login, **`WriteAsync`**, return the **same** `LoginResponse` (no new JWT). This API does not implement a server-side `force` refresh path.
|
|
||||||
6. **If access expired** but refresh is still valid (`UtcNow <= RefreshTokenExpiresAt`): issue a **new** access JWT + new refresh token, upsert token, save, return new `LoginResponse`.
|
|
||||||
7. **If refresh is expired**: remove that token record, return **401** “Refresh token has expired.”
|
|
||||||
|
|
||||||
### 3.4 Logout
|
|
||||||
|
|
||||||
**Endpoint:** `POST /api/identity/logout` (**requires** `JwtAuthorizationFilter` — send `Authorization: Bearer`)
|
|
||||||
**Controller:** `IdentityController.Logout` → `IdentityService.Logout`
|
|
||||||
|
|
||||||
1. **Resolve user** by **access JWT** from the validated Bearer token (`GetByAccessTokenAsync` / token string from JWT context).
|
|
||||||
2. If found: **`LogoutFromAllDevices`** → `RevokeAllJwtTokens()`; else → `RemoveJwtToken(accessToken)` for the current session.
|
|
||||||
3. **`WriteAsync`** if the user was updated.
|
|
||||||
4. Return success (implementation may still return OK if the token was unknown—clients should clear local state regardless).
|
|
||||||
|
|
||||||
**Request body (`LogoutRequest`):** `token` (access JWT, for shared model parity), `logoutFromAllDevices` — the server uses the **Bearer** access token for lookup.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Frontend flow
|
|
||||||
|
|
||||||
### 4.1 Identity state and storage
|
|
||||||
|
|
||||||
**Redux:** `identitySlice` (`src/MaksIT.WebUI/src/redux/slices/identitySlice.ts`)
|
|
||||||
|
|
||||||
- **State:** `identity: Identity | null`, `hydrated: boolean`, `status`, `showUserOffcanvas`.
|
|
||||||
- **Persistence:** Login/refresh responses are written to **localStorage** via `writeIdentity`; on load, `setIdentityFromLocalStorage` reads and hydrates state and **enriches** from JWT claims (`userId`, `username`, `roles`, `isGlobalAdmin`, `acls` when those claims exist).
|
|
||||||
|
|
||||||
**Identity type** extends `LoginResponse` with client-side fields: `userId`, `username`, `roles`, `isGlobalAdmin`, `acls`.
|
|
||||||
|
|
||||||
### 4.2 Login UI
|
|
||||||
|
|
||||||
**Component:** `LoginScreen` (`src/MaksIT.WebUI/src/components/LoginScreen.tsx`)
|
|
||||||
|
|
||||||
- Form: username and password; **2FA** inputs are **commented out** until the backend supports them.
|
|
||||||
- On submit: `dispatch(login(loginRequest))`.
|
|
||||||
- On successful login, `identitySlice` stores the response in state and localStorage; `LoginScreen` redirects when identity is present and refresh token is not expired.
|
|
||||||
|
|
||||||
### 4.3 Route protection
|
|
||||||
|
|
||||||
**Component:** `Authorization` (`src/MaksIT.WebUI/src/components/Authorization.tsx`)
|
|
||||||
|
|
||||||
- Wraps protected routes.
|
|
||||||
- On mount, if not hydrated, dispatches `setIdentityFromLocalStorage()`.
|
|
||||||
- **When hydrated:** if `identity` is missing or `refreshTokenExpiresAt` is in the past, redirects to `/login` (with `state.from` for return URL).
|
|
||||||
- Renders children only when hydrated and refresh token is not expired.
|
|
||||||
|
|
||||||
**Refresh token** expiry is what forces re-login; the **access** token may expire while refresh is still valid (handled by axios).
|
|
||||||
|
|
||||||
### 4.4 Axios: token attachment and refresh
|
|
||||||
|
|
||||||
**File:** `src/MaksIT.WebUI/src/axiosConfig.ts`
|
|
||||||
|
|
||||||
- **Excluded URLs** (no Bearer token, no refresh loop): login and refresh routes (`GetApiRoute(ApiRoutes.identityLogin).route`, `GetApiRoute(ApiRoutes.identityRefresh).route`).
|
|
||||||
- **Request interceptor:** If access token is expired but refresh is still valid by client clock, await a single shared `refreshJwt()`; on success attach new `Authorization`; on failure dispatch `clearIdentity()` and reject (do not send protected calls with an expired access token).
|
|
||||||
- **Response interceptor:** On **401**, optionally retry once after refresh when refresh is still valid; on refresh failure, `clearIdentity()`.
|
|
||||||
- **Serialization:** `isRefreshing` / `refreshPromise` so concurrent requests share one refresh.
|
|
||||||
|
|
||||||
### 4.5 Async thunks and clearIdentity
|
|
||||||
|
|
||||||
- **`login`:** POST login; on success writes identity and enriches from JWT.
|
|
||||||
- **`refreshJwt(force?)`:** POST refresh with `refreshToken` (and optional `force` in body for shared code paths; server ignores `force` on Certs). On failure, identity cleared.
|
|
||||||
- **`logout`:** POST logout with access token in body, then clear state/localStorage.
|
|
||||||
- **`clearIdentity()`:** Clears Redux and localStorage without calling logout API (used when refresh fails).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. API summary
|
|
||||||
|
|
||||||
| Method | Endpoint | Bearer required | Purpose |
|
|
||||||
|--------|----------|-----------------|--------|
|
|
||||||
| POST | `/api/identity/login` | No | Login; returns access + refresh token. |
|
|
||||||
| POST | `/api/identity/refresh` | No | Exchange refresh token for same or new tokens. |
|
|
||||||
| POST | `/api/identity/logout` | Yes | Revoke session(s); Bearer identifies the access token to remove. |
|
|
||||||
|
|
||||||
Other identity routes (e.g. `PATCH /api/identity/user/{id}`) use `JwtAuthorizationFilter` and require a valid JWT.
|
|
||||||
|
|
||||||
Base route: `api/identity` (`IdentityController`, `AppMap`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Sequence overview
|
|
||||||
|
|
||||||
**Login:** User submits credentials → POST `/api/identity/login` → user row updated with new `JwtToken` in PostgreSQL → WebUI stores identity → redirect into app.
|
|
||||||
|
|
||||||
**Authenticated request (access token valid):** Interceptor adds `Authorization: Bearer` → API validates JWT.
|
|
||||||
|
|
||||||
**Access expired, refresh valid:** Interceptor awaits `refreshJwt()` → POST `/api/identity/refresh` → updated identity → original request retried with new token.
|
|
||||||
|
|
||||||
**401 on protected request:** Response interceptor attempts refresh; if refresh returns 401, `clearIdentity()` and redirect to `/login`.
|
|
||||||
|
|
||||||
**Logout:** POST `/api/identity/logout` with `Authorization: Bearer` and body `{ token, logoutFromAllDevices }` → server removes token(s) from the user row → client clears storage.
|
|
||||||
|
|
||||||
Use Certs resources in examples (e.g. **accounts**, **certificate flows**): no protected API should run after refresh has failed without clearing identity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Security notes
|
|
||||||
|
|
||||||
- **Passwords** use salt + server-side **pepper**; not stored in plain text.
|
|
||||||
- **Refresh tokens** are stored per user in PostgreSQL; expiry and invalidation are enforced in `IdentityService.RefreshTokenAsync`.
|
|
||||||
- **2FA** is **not** implemented on the Certs WebAPI; do not enable the WebUI 2FA fields until backend support exists.
|
|
||||||
- **Login/refresh** do not require Bearer; **logout** and other protected identity routes use `JwtAuthorizationFilter`.
|
|
||||||
- Frontend keeps **one** identity in localStorage; refresh is serialized to avoid duplicate refresh storms.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Key files reference
|
|
||||||
|
|
||||||
| Area | File |
|
|
||||||
|------|------|
|
|
||||||
| Domain – User | `src/MaksIT.CertsUI/Domain/User.cs` |
|
|
||||||
| Domain – JwtToken | `src/MaksIT.CertsUI/Domain/JwtToken.cs` |
|
|
||||||
| API service | `src/MaksIT.CertsUI/Services/IdentityService.cs` |
|
|
||||||
| API controller | `src/MaksIT.CertsUI/Controllers/IdentityController.cs` |
|
|
||||||
| API models | `src/MaksIT.Models/LetsEncryptServer/Identity/Login/`, `.../Logout/` |
|
|
||||||
| Frontend – state | `src/MaksIT.WebUI/src/redux/slices/identitySlice.ts` |
|
|
||||||
| Frontend – HTTP | `src/MaksIT.WebUI/src/axiosConfig.ts` |
|
|
||||||
| Frontend – routes | `src/MaksIT.WebUI/src/components/Authorization.tsx` |
|
|
||||||
| Frontend – login UI | `src/MaksIT.WebUI/src/components/LoginScreen.tsx` |
|
|
||||||
| Frontend – API map | `src/MaksIT.WebUI/src/AppMap.tsx` |
|
|
||||||
@ -1,251 +0,0 @@
|
|||||||
# PATCH Delta Handling – Backend & Frontend Reference
|
|
||||||
|
|
||||||
This document is the **single reference** for how PATCH payloads (deltas) are structured and interpreted so backend (BE) and frontend (FE) stay consistent. It follows the **MaksIT.Core** patch contract (same rules as shared **`deepDelta`** helpers). **MaksIT-CertsUI** uses that model with a **hostnames** collection on account PATCH.
|
|
||||||
|
|
||||||
**Audience:** Backend (C# / ASP.NET) and Frontend (TypeScript / React) developers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TL;DR (start here)
|
|
||||||
|
|
||||||
- **PATCH** sends only **what changed**, not the full resource. Each change is tagged with an **operation** (set, remove, add item, remove item).
|
|
||||||
- **Root fields** (e.g. `description`, `contact`): send new value + `operations["fieldName"] = SetField` or `RemoveField`.
|
|
||||||
- **Collections** (e.g. `hostnames`): **do not** replace the whole array when the API is “patchable collection” semantics. Send **per-item** changes: each added item has `operations.collectionItemOperation = AddToCollection`, each removed item has `RemoveFromCollection`, and changed items send identity and changed fields.
|
|
||||||
- **Frontend (Certs WebUI):** For **Edit Account**, use
|
|
||||||
`deepDelta(formState, backupState, { arrays: { hostnames: { identityKey: 'hostname', idFieldKey: 'hostname' } } })`
|
|
||||||
so hostname rows are itemized (including “add first hostname”) and stay in sync with the backend.
|
|
||||||
- **Backend:** Use `TryGetOperation(Constants.CollectionItemOperation, out var op)` on each collection item; never treat root `Operations["hostnames"] = SetField` as “replace all” if the API follows per-item patch semantics.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Core contract (MaksIT.Core)
|
|
||||||
|
|
||||||
The following come from **MaksIT.Core** and must be respected by all consumers.
|
|
||||||
|
|
||||||
### 1.1 PatchOperation enum
|
|
||||||
|
|
||||||
| Value | Integer | Meaning |
|
|
||||||
|-------|---------|--------|
|
|
||||||
| `SetField` | 0 | Set or replace a scalar or root-level value |
|
|
||||||
| `RemoveField` | 1 | Set a field to null |
|
|
||||||
| `AddToCollection` | 2 | Add an item to a collection (used on **collection items**, not root) |
|
|
||||||
| `RemoveFromCollection` | 3 | Remove an item from a collection (used on **collection items**, not root) |
|
|
||||||
|
|
||||||
- **Source:** `MaksIT.Core.Webapi.Models.PatchOperation`
|
|
||||||
- **FE mirror:** `PatchOperation` enum in WebUI (`src/MaksIT.WebUI/src/models/PatchOperation.ts`) must keep the same numeric values for JSON serialization.
|
|
||||||
|
|
||||||
### 1.2 PatchRequestModelBase
|
|
||||||
|
|
||||||
- **Operations:** `Dictionary<string, PatchOperation>?` (C#) / `{ [key: string]: PatchOperation }` (TS).
|
|
||||||
- **Lookup:** Case-insensitive by **property name** (e.g. `"hostnames"`, `"description"`).
|
|
||||||
- **Usage:**
|
|
||||||
- **Root level:** `Operations["propertyName"]` describes the operation for that property (e.g. `SetField` for a changed field, `RemoveField` for null).
|
|
||||||
- **Collection items:** Each element of a collection property is itself a patch model; it uses a **reserved key** (see below) to indicate add/remove/update for that item.
|
|
||||||
|
|
||||||
### 1.3 Collection item operation key
|
|
||||||
|
|
||||||
For **elements inside a collection property** (e.g. each item in `hostnames`), the operation is stored under a fixed key so the backend can distinguish “add/remove this item” from “update fields of this item”.
|
|
||||||
|
|
||||||
- **Key name:** `collectionItemOperation`
|
|
||||||
- **BE:** `Constants.CollectionItemOperation` (same string across MaksIT services when aligned with Core).
|
|
||||||
- **FE:** `COLLECTION_ITEM_OPERATION` in `src/MaksIT.WebUI/src/models/PatchOperation.ts`; same string in payloads and in `deepDelta`. Keep in sync with backend.
|
|
||||||
|
|
||||||
**Allowed values for collection items:** `AddToCollection` (2), `RemoveFromCollection` (3). For in-place updates (same item, changed fields), the item typically has an `id` and no `collectionItemOperation`, or field-level changes follow your API’s semantics.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Backend (BE) rules
|
|
||||||
|
|
||||||
### 2.1 Root-level properties
|
|
||||||
|
|
||||||
- Read `request.TryGetOperation(propertyName, out var op)`.
|
|
||||||
- If `op == SetField`: apply the new value from the request for that property.
|
|
||||||
- If `op == RemoveField`: set the property to null (or clear it) where applicable.
|
|
||||||
- If the property is not in `Operations` but is present with a value, treat as optional direct assignment (or ignore), depending on your API convention; for strict PATCH, prefer requiring operations for changed fields.
|
|
||||||
|
|
||||||
### 2.2 Collection properties (e.g. hostnames)
|
|
||||||
|
|
||||||
- **Rule:** Collection properties are patched **only via per-item operations** when the API follows patchable-collection semantics. The backend does **not** treat root `Operations["hostnames"] = SetField` as “replace the entire collection” unless explicitly documented for that endpoint.
|
|
||||||
- For each item in the collection payload:
|
|
||||||
1. Call `item.TryGetOperation(Constants.CollectionItemOperation, out var collectionOp)` (or the agreed constant).
|
|
||||||
2. If `collectionOp == AddToCollection`: add the item to the collection (merge by id if present, or append).
|
|
||||||
3. If `collectionOp == RemoveFromCollection`: remove the item (by `item.Id` or by matching key fields such as hostname string).
|
|
||||||
4. If no `collectionItemOperation` but the item is identifiable: treat as **in-place update**.
|
|
||||||
5. If no `collectionItemOperation` and the item cannot be identified: **do not add** ambiguous items; the FE must send `AddToCollection` for new rows when required by your rules.
|
|
||||||
|
|
||||||
### 2.3 Consistency checklist (BE)
|
|
||||||
|
|
||||||
- [ ] Use the same `CollectionItemOperation` key as the FE (see Constants / Core).
|
|
||||||
- [ ] Do not rely on root-level `SetField` for patchable collections to mean “replace all”; use per-item add/remove/update only (unless documented otherwise).
|
|
||||||
- [ ] For add: require `TryGetOperation(CollectionItemOperation) == AddToCollection` (or equivalent) for new items where applicable.
|
|
||||||
- [ ] For remove: use `RemoveFromCollection` and/or identity fields agreed with the FE.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Frontend (FE) rules
|
|
||||||
|
|
||||||
### 3.1 Building the delta (deepDelta)
|
|
||||||
|
|
||||||
- **Scalar / root fields:** Emit the new value and set `operations[propertyName] = SetField` or `RemoveField` as appropriate.
|
|
||||||
- **Primitive arrays:** Emit the full array and `operations[propertyName] = SetField` (full replace).
|
|
||||||
- **Object arrays that are “patchable collections”:** Must always produce **itemized deltas** when configured with an **array policy** (identity key / id field):
|
|
||||||
- Each added item must have `operations.collectionItemOperation = AddToCollection`.
|
|
||||||
- Each removed item must have `operations.collectionItemOperation = RemoveFromCollection`.
|
|
||||||
- Updated items carry changed fields and identity; see `deepDelta` implementation in `src/MaksIT.WebUI/src/functions/deep/deepDelta.ts`.
|
|
||||||
|
|
||||||
### 3.2 Patchable collections – identity requirement
|
|
||||||
|
|
||||||
For the backend to interpret add/remove/update correctly, each collection **item** must be identifiable:
|
|
||||||
|
|
||||||
- **Existing items:** Use `id` from the server when present.
|
|
||||||
- **New items:** May have no server `id`; the FE must pass an **array policy** with `identityKey` / `idFieldKey` so the delta stays itemized (e.g. hostname string as stable key for hostnames).
|
|
||||||
|
|
||||||
### 3.3 Shared array policies (this repository)
|
|
||||||
|
|
||||||
**MaksIT-CertsUI** does **not** ship a shared `patchCollectionPolicies.ts`; the **Edit Account** form passes an **inline** policy for the `hostnames` collection:
|
|
||||||
|
|
||||||
| Collection | Policy (inline) | Used in |
|
|
||||||
|------------|-----------------|---------|
|
|
||||||
| `hostnames` | `{ identityKey: 'hostname', idFieldKey: 'hostname' }` | `EditAccount.tsx` |
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
deepDelta(fromFormState, fromBackupState, {
|
|
||||||
arrays: {
|
|
||||||
hostnames: {
|
|
||||||
identityKey: 'hostname',
|
|
||||||
idFieldKey: 'hostname',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.4 Consistency checklist (FE)
|
|
||||||
|
|
||||||
- [ ] Use the same `PatchOperation` numeric values as Core (SetField 0, RemoveField 1, AddToCollection 2, RemoveFromCollection 3).
|
|
||||||
- [ ] Use `COLLECTION_ITEM_OPERATION` from `PatchOperation.ts` (same string as backend `Constants.CollectionItemOperation`).
|
|
||||||
- [ ] For account `hostnames`, pass the `hostnames` array policy in `deepDelta` so the delta is **itemized**, not a blind full-replace of the array.
|
|
||||||
- [ ] New items must have `operations.collectionItemOperation = AddToCollection` when the backend expects it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Payload examples
|
|
||||||
|
|
||||||
### 4.1 Root-level SetField (scalar)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"description": "Updated",
|
|
||||||
"operations": {
|
|
||||||
"description": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`0` = SetField.
|
|
||||||
|
|
||||||
### 4.2 Root-level RemoveField (clear optional field)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"operations": { "someOptionalField": 1 }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`1` = RemoveField.
|
|
||||||
|
|
||||||
### 4.3 Root-level SetField (primitive array – full replace)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tags": ["a", "b", "c"],
|
|
||||||
"operations": {
|
|
||||||
"tags": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.4 Collection property – itemized (add items)
|
|
||||||
|
|
||||||
Example shape for new hostname rows (numeric ops match `PatchOperation` enum):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"hostnames": [
|
|
||||||
{
|
|
||||||
"hostname": "api.example.com",
|
|
||||||
"isDisabled": false,
|
|
||||||
"operations": { "collectionItemOperation": 2 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`2` = AddToCollection.
|
|
||||||
|
|
||||||
### 4.5 Collection property – remove item
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"hostnames": [
|
|
||||||
{
|
|
||||||
"hostname": "old.example.com",
|
|
||||||
"operations": { "collectionItemOperation": 3 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`3` = RemoveFromCollection (identity may be `hostname` or server `id` depending on API).
|
|
||||||
|
|
||||||
### 4.6 Collection property – in-place update
|
|
||||||
|
|
||||||
Item exists; fields change; no `collectionItemOperation` on the item (or only nested field operations per your model).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Quick reference
|
|
||||||
|
|
||||||
| Aspect | Backend | Frontend |
|
|
||||||
|--------|---------|----------|
|
|
||||||
| Operation enum | `PatchOperation` (Core) | `PatchOperation` (same values 0–3) |
|
|
||||||
| Root operations | `TryGetOperation(propertyName, out op)` | `operations[propertyName] = op` |
|
|
||||||
| Collection item key | `Constants.CollectionItemOperation` (`"collectionItemOperation"`) | `COLLECTION_ITEM_OPERATION` in payload and deepDelta |
|
|
||||||
| New collection item | Require `AddToCollection` on item when applicable | Send `operations.collectionItemOperation: 2` for new items |
|
|
||||||
| Certs account hostnames | Per-item patch semantics | `deepDelta` + `hostnames` array policy in `EditAccount.tsx` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Related docs
|
|
||||||
|
|
||||||
- **MaksIT.Core:** `PatchOperation`, `PatchRequestModelBase` (README / XML).
|
|
||||||
- [User and API key RBAC](./USER_AND_API_KEY_RBAC.md) · [RBAC reference](./RBAC_REFERENCE.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Current implementation vs reference (MaksIT-CertsUI)
|
|
||||||
|
|
||||||
### 7.1 Frontend (FE)
|
|
||||||
|
|
||||||
**Checked:** `deepDelta` usage in `src/MaksIT.WebUI/src/forms/EditAccount.tsx` for account PATCH; inline `hostnames` array policy; `COLLECTION_ITEM_OPERATION` in `PatchOperation.ts` and usage in `deepDelta.ts`.
|
|
||||||
|
|
||||||
- **Safe:** Hostname rows use `identityKey` / `idFieldKey` so itemized deltas include `AddToCollection` / `RemoveFromCollection` where appropriate.
|
|
||||||
- **Consistent:** Same `PatchOperation` values and collection-item key string as Core.
|
|
||||||
|
|
||||||
**FE summary:** Follows the shared reference; account PATCH covers **account fields and hostnames**.
|
|
||||||
|
|
||||||
### 7.2 Backend (BE)
|
|
||||||
|
|
||||||
**Maintainers:** Confirm the account PATCH handler in MaksIT.CertsUI WebAPI applies the same per-item rules (`TryGetOperation(CollectionItemOperation, ...)`) for `hostnames` as described in sections 2.2 and 3.
|
|
||||||
|
|
||||||
### 7.3 Gaps and maintenance
|
|
||||||
|
|
||||||
| Topic | Status | Note |
|
|
||||||
|-------|--------|------|
|
|
||||||
| Shared policies file | N/A in Certs | Inline policy in `EditAccount.tsx` for `hostnames`. |
|
|
||||||
| New forms with patchable collections | **Ongoing** | When adding a form that patches a collection, pass the correct `arrays: { key: policy }` to `deepDelta`. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: 2026-04-12*
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
# PowerShell client module (`MaksIT.CertsUI.Client.PowerShell`)
|
|
||||||
|
|
||||||
PowerShell module that exposes the **MaksIT CertsUI API** via custom cmdlets, built on **MaksIT.CertsUI.Client** (C# / .NET).
|
|
||||||
|
|
||||||
**Source:** `src/MaksIT.CertsUI.Client.PowerShell/` · **Auth & routes:** [USER_AND_API_KEY_RBAC.md](./USER_AND_API_KEY_RBAC.md) · **Permission matrices:** [RBAC_REFERENCE.md](./RBAC_REFERENCE.md) · **E2E:** [src/e2e-tests/README.md](../../src/e2e-tests/README.md) · **Repo entry:** [README.md](../../README.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- **Latest [PowerShell 7](https://github.com/PowerShell/PowerShell/releases)** with a **.NET 10** host. **pwsh 7.4 on .NET 8** cannot load this **`net10.0`** module.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
[System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription
|
|
||||||
# expect: .NET 10.0.x
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. Build the solution (or the `MaksIT.CertsUI.Client.PowerShell` project).
|
|
||||||
2. Import from build output:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Import-Module .\src\MaksIT.CertsUI.Client.PowerShell\bin\Debug\net10.0\MaksIT.CertsUI.Client.PowerShell.psd1 -Force
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. **Connect** (base URL is the public ingress or YARP root, e.g. `http://localhost:8080` — no `/api` suffix):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Connect-CertsUI -BaseAddress "http://localhost:8080" -ApiKey "your-api-key"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Health and accounts:**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Test-CertsUIHealth
|
|
||||||
Get-CertsUIAccounts
|
|
||||||
Get-CertsUIAccount -AccountId <guid>
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **HA / load-balancer check:**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Get-CertsUIRuntimeInstanceId
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Create / patch / delete account** (create runs the full ACME flow — use staging for tests):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Invoke-CertsUICreateAccount -Description "e2e" -Contacts "mailto:a@b" -ChallengeType "http-01" -Hostnames "example.com" -IsStaging -AgreeToS
|
|
||||||
Invoke-CertsUIPatchAccount -AccountId <guid> -Description "updated"
|
|
||||||
Invoke-CertsUIDeleteAccount -AccountId <guid>
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Disconnect:**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Disconnect-CertsUI
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cmdlets
|
|
||||||
|
|
||||||
| Cmdlet | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| `Connect-CertsUI` | Set base URL and API key for the session |
|
|
||||||
| `Disconnect-CertsUI` | Clear session |
|
|
||||||
| `Test-CertsUIHealth` | `GET /health/live` and `/health/ready` (ready returns 503 until migrations and bootstrap finish) |
|
|
||||||
| `Get-CertsUIAccounts` | `GET /api/accounts` |
|
|
||||||
| `Get-CertsUIAccount` | `GET /api/account/{id}` |
|
|
||||||
| `Get-CertsUIRuntimeInstanceId` | `GET /api/debug/runtime-instance-id` |
|
|
||||||
| `Invoke-CertsUICreateAccount` | `POST /api/account` |
|
|
||||||
| `Invoke-CertsUIPatchAccount` | `PATCH /api/account/{id}` |
|
|
||||||
| `Invoke-CertsUIDeleteAccount` | `DELETE /api/account/{id}` |
|
|
||||||
|
|
||||||
## E2E scenarios
|
|
||||||
|
|
||||||
PowerShell scenarios under [`src/e2e-tests/`](../src/e2e-tests/) build this module and run registered tests. See [src/e2e-tests/README.md](../../src/e2e-tests/README.md).
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
# RBAC reference — scopes and permission matrices
|
|
||||||
|
|
||||||
This document is the **policy reference** for **MaksIT.CertsUI**: scope flags, shorthand roles, and **who may perform which actions** on Identity, API-key management, and ACME/certificate endpoints.
|
|
||||||
|
|
||||||
**Read first:** [User vs API key RBAC](./USER_AND_API_KEY_RBAC.md) — how JWT and `X-API-KEY` are resolved, `CertsUIAuthorizationFilter` on all protected routes, `GetActingJwtTokenData`, global admin on user vs key, and troubleshooting.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Scope model
|
|
||||||
|
|
||||||
Storage and wire shape follow **MaksIT.Vault** (organization id + flags on `user_entity_scopes` / `api_key_entity_scopes`). CertsUI uses a **smaller** scope type enum than Vault.
|
|
||||||
|
|
||||||
Each **scope** is:
|
|
||||||
|
|
||||||
| Field | Meaning |
|
|
||||||
|-------|---------|
|
|
||||||
| **`EntityId`** | **Organization** id this grant applies to (same as Vault org scopes) |
|
|
||||||
| **`EntityType`** | **`Identity`** or **`ApiKey`** — which *administration* surface the flags govern (not “organization” as a type) |
|
|
||||||
| **`Scope`** | `ScopePermission` flags bitmask |
|
|
||||||
|
|
||||||
### 1.1 Permission flags (`ScopePermission`)
|
|
||||||
|
|
||||||
| Flag | Value | Typical use |
|
|
||||||
|------|-------|-------------|
|
|
||||||
| `Read` | `1 << 0` | List/search and read in that org for the given `EntityType` |
|
|
||||||
| `Write` | `1 << 1` | Patch targets tied to that org |
|
|
||||||
| `Delete` | `1 << 2` | Delete targets tied to that org |
|
|
||||||
| `Create` | `1 << 3` | Create principals whose requested scopes include that org |
|
|
||||||
|
|
||||||
**Check:** `RbacHelpers.Has(granted, required)` — all bits in `required` must be set.
|
|
||||||
|
|
||||||
**Search/list:** RBAC filters results in **`IdentityService`** / **`ApiKeyService`** query predicates first; request/UI filters apply on top. The acting user is excluded from user search.
|
|
||||||
|
|
||||||
A principal may hold many scopes; effective access is the **union** of matching rows.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Shorthand columns (matrices below)
|
|
||||||
|
|
||||||
Tables use **Admin** (global, no scopes required) and two **scoped** columns keyed by organization **O**:
|
|
||||||
|
|
||||||
| Column | Meaning |
|
|
||||||
|--------|---------|
|
|
||||||
| **Identity manager (org O)** | `EntityType = Identity`, `EntityId = O`, with `Read` + `Write` + `Create` / `Delete` as required by the action |
|
|
||||||
| **Identity reader (org O)** | `EntityType = Identity`, `EntityId = O`, with `Read` only |
|
|
||||||
|
|
||||||
For API-key administration, replace **Identity** with **ApiKey** in the column names (**API key manager** / **API key reader** on org **O**).
|
|
||||||
|
|
||||||
CertsUI has **no** Vault-style Application or Secret scopes; certificate work is not scoped per org in the database today.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Global administrator
|
|
||||||
|
|
||||||
A **global administrator** is a JWT user with **`IsGlobalAdmin`** or an API key with **`IsGlobalAdmin`** on the key row (independent flags — see [User vs API key RBAC](./USER_AND_API_KEY_RBAC.md) §2.1).
|
|
||||||
|
|
||||||
Global admins bypass scoped checks in `RBACWrapper*` helpers. Only global admins may **assign or remove** `IsGlobalAdmin` on users or keys (`RbacHelpers`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Identity (users)
|
|
||||||
|
|
||||||
Target users carry **entity scopes** (org ids + flags). A scoped caller must cover **every organization** the target belongs to (same rule as Vault identity).
|
|
||||||
|
|
||||||
### 4.1 Enforced in code today (source of truth)
|
|
||||||
|
|
||||||
| Area | Global admin | Non–global-admin |
|
|
||||||
|------|--------------|------------------|
|
|
||||||
| **Search users / user scopes** | All (minus self in user search) | Only users whose org ids ⊆ actor’s org ids with **`Identity` + `Read`** |
|
|
||||||
| **Create / patch `IsGlobalAdmin`** | Allowed | **Forbidden** if request touches `IsGlobalAdmin` |
|
|
||||||
| **Read / create / patch / delete user by id** | Allowed | **RBAC wrapper returns success without org checks** — scoped enforcement **not** implemented in `ReadUserRBAC` / `CreateUserRBAC` / `PatchUserRBAC` / `DeleteUserRBAC` lambdas yet (XML comments describe the **intended** Vault-aligned rules) |
|
|
||||||
|
|
||||||
If shorthand tables below disagree with §4.1, **§4.1 wins** until CRUD wrappers are brought in line with Vault (`GetEntityIdsWithScope` checks like `maksit-vault` `IdentityService`).
|
|
||||||
|
|
||||||
### 4.2 Intended policy (target behavior; align CRUD with this)
|
|
||||||
|
|
||||||
| Action | Admin | Identity manager | Identity reader |
|
|
||||||
|--------|-------|------------------|-----------------|
|
|
||||||
| Read user | Yes (any) | Yes if `Read` on **Identity** for **all** target orgs | Yes if `Read` on **all** target orgs |
|
|
||||||
| Create user | Yes (any) | Yes if `Create` on **Identity** for **all** orgs in create request | No |
|
|
||||||
| Patch user | Yes (any) | Yes if `Write` on **all** target orgs (and touched orgs on scope patch) | Self only for profile fields; no role/org changes |
|
|
||||||
| Delete user | Yes (any, not self) | Yes if `Delete` on **all** target orgs; not self | No |
|
|
||||||
|
|
||||||
**Self:** Vault allows self read/patch with restrictions; Certs **search** excludes self; self-service rules for patch should match Vault when CRUD is completed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. ACME, accounts, cache, and agent
|
|
||||||
|
|
||||||
There is **no** per-organization scope on certificate accounts or ACME sessions in the current schema.
|
|
||||||
|
|
||||||
### 5.1 Enforced permissions (from code)
|
|
||||||
|
|
||||||
| Resource / area | Routes | Non–global-admin JWT | Non–global-admin API key |
|
|
||||||
|-----------------|--------|----------------------|---------------------------|
|
|
||||||
| Accounts | `/api/account/...` | Any authenticated principal | Any authenticated principal |
|
|
||||||
| ACME flow | `/api/certs/...` | Any authenticated principal | Any authenticated principal |
|
|
||||||
| Registration cache | `/api/cache/...` | Any authenticated principal | Any authenticated principal |
|
|
||||||
| Agent hello | `/api/agent/...` | Any authenticated principal | Any authenticated principal |
|
|
||||||
| HTTP-01 challenge | `GET /.well-known/acme-challenge/...` | **Anonymous** (no filter) | **Anonymous** |
|
|
||||||
|
|
||||||
All of the above use `RBACWrapper(..., _ => Result.Ok(), _ => Result.Ok())` except Well-Known. **Treat every valid API key like a full automation principal** for certificate operations until account-scoped RBAC exists.
|
|
||||||
|
|
||||||
### 5.2 Operational guidance
|
|
||||||
|
|
||||||
| Goal | Recommendation |
|
|
||||||
|------|----------------|
|
|
||||||
| Least privilege for **cert automation** | Issue keys only to trusted pipelines; rotate and expire keys; network-restrict the API. Do not rely on org scopes for ACME yet. |
|
|
||||||
| Least privilege for **user/key admin** | Use scoped **Identity** / **ApiKey** grants; use **search** for operators without global admin. |
|
|
||||||
| Full platform control | Global-admin JWT or global-admin API key. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Managing API keys
|
|
||||||
|
|
||||||
Routes: `/api/apikey/...`. Callers use **`GetActingJwtTokenData()`** — JWT **or** API key (see [User vs API key RBAC](./USER_AND_API_KEY_RBAC.md) §5).
|
|
||||||
|
|
||||||
### 6.1 Enforced in code today
|
|
||||||
|
|
||||||
Same pattern as §4.1:
|
|
||||||
|
|
||||||
| Area | Global admin | Non–global-admin |
|
|
||||||
|------|--------------|------------------|
|
|
||||||
| **Search keys / key scopes** | All | Keys whose org ids ⊆ actor’s org ids with **`ApiKey` + `Read`** |
|
|
||||||
| **Patch `IsGlobalAdmin` on a key** | Allowed | **Forbidden** |
|
|
||||||
| **Read / create / patch / delete key by id** | Allowed | **No org scope check in RBAC lambdas yet** |
|
|
||||||
|
|
||||||
### 6.2 Intended policy (target behavior)
|
|
||||||
|
|
||||||
| Action | Admin | API key manager | API key reader |
|
|
||||||
|--------|-------|-----------------|---------------|
|
|
||||||
| Read API key | Yes (any) | Yes if `Read` on **ApiKey** for **all** key orgs | Yes if `Read` on **all** key orgs |
|
|
||||||
| Create API key | Yes (any); may set key `IsGlobalAdmin` | Yes if `Create` on **all** orgs in request; cannot set key `IsGlobalAdmin` | No |
|
|
||||||
| Patch API key | Yes (any) | Yes if `Write` on **all** key orgs | No |
|
|
||||||
| Delete API key | Yes (any) | Yes if `Delete` on **all** key orgs | No |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Calling the API with an API key
|
|
||||||
|
|
||||||
Use **`X-API-KEY`** on any protected route that uses **`CertsUIAuthorizationFilter`**. RBAC uses the **key’s** scopes and **`IsGlobalAdmin` on the key** — not the human operator’s JWT.
|
|
||||||
|
|
||||||
| Use case | Key type |
|
|
||||||
|----------|----------|
|
|
||||||
| PowerShell / `MaksIT.CertsUI.Client` automation (accounts, ACME) | Any valid key (prefer dedicated non-global keys only if you accept §5.1) |
|
|
||||||
| Create accounts, run `FullFlow`, cache | Authenticated key (global admin not required today) |
|
|
||||||
| Manage users or API keys via API | Key needs appropriate **Identity** / **ApiKey** scopes or global admin |
|
|
||||||
| Match WebUI “full admin” for certs + identity | **`IsGlobalAdmin: true`** on the key |
|
|
||||||
|
|
||||||
Details: [User vs API key RBAC](./USER_AND_API_KEY_RBAC.md) §1–2, [POWERSHELL_CLIENT_MODULE.md](./POWERSHELL_CLIENT_MODULE.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Comparison with MaksIT.Vault
|
|
||||||
|
|
||||||
| Topic | Vault | CertsUI |
|
|
||||||
|-------|-------|---------|
|
|
||||||
| Dual auth filter | `VaultAuthorizationFilter` on `/api/vault/...` only | `CertsUIAuthorizationFilter` on all protected API controllers |
|
|
||||||
| Identity / API key routes | JWT only | JWT **or** API key via `GetActingJwtTokenData()` |
|
|
||||||
| Resource scopes | Organization, Application, secrets | **None** for ACME; Identity/ApiKey admin only |
|
|
||||||
| Scoped CRUD on identity | Enforced in `IdentityService` | **Search enforced**; CRUD wrappers **stub** (May 2026) |
|
|
||||||
|
|
||||||
When porting fixes from Vault, copy **`GetEntityIdsWithScope`** patterns from `maksit-vault` `IdentityService` / `APIKeyService` into this repo’s RBAC lambdas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: May 2026*
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
# Reverse proxy routing (YARP)
|
|
||||||
|
|
||||||
The **`ReverseProxy`** project ([`src/ReverseProxy`](../../src/ReverseProxy)) is an **ASP.NET + YARP** edge that listens on **port 8080** in Docker Compose (`docker-compose.override.yml` maps `8080:8080`).
|
|
||||||
|
|
||||||
**Config:** [`src/ReverseProxy/appsettings.json`](../../src/ReverseProxy/appsettings.json)
|
|
||||||
|
|
||||||
**Certs routing:** **`/.well-known/acme-challenge/`** is matched **before** the SPA catch-all so HTTP-01 challenges reach the WebAPI on the **same public host** as the UI.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Route table
|
|
||||||
|
|
||||||
Routes use explicit **`Order`** (lower = matched first) so the SPA catch-all never wins over `/api`, `/swagger`, or `/.well-known/` when JSON key order varies.
|
|
||||||
|
|
||||||
Compose service names are **`server`** (WebAPI) and **`client`** (Vite/WebUI). Cluster IDs **`webapiCluster`** / **`webuiCluster`** reference those upstreams.
|
|
||||||
|
|
||||||
| Order | Path match | Cluster | Upstream (Compose) |
|
|
||||||
|------|------------|---------|---------------------|
|
|
||||||
| 5 | `/.well-known/acme-challenge/{**catch-all}` | `webapiCluster` | `http://server:5000/` |
|
|
||||||
| 10 | `/swagger/{**catch-all}` | `webapiCluster` | `http://server:5000/` |
|
|
||||||
| 20 | `/api/{**catch-all}` | `webapiCluster` | `http://server:5000/` |
|
|
||||||
| 1000 | `/{**catch-all}` | `webuiCluster` | `http://client:5173/` |
|
|
||||||
|
|
||||||
YARP forwards the **same path** to the destination. Example:
|
|
||||||
|
|
||||||
- Client: `POST http://localhost:8080/api/identity/login`
|
|
||||||
- Proxied to: `POST http://server:5000/api/identity/login`
|
|
||||||
|
|
||||||
Controllers use the usual **`/api/...`** prefix (e.g. `api/identity`, account and certificate flows)—there is **no** `api/vault`-style segment. Locally, the Web UI uses `public/config.js` / `.env` with `http://localhost:8080/api` so XHR calls go **through** YARP.
|
|
||||||
|
|
||||||
### HTTP-01 (Let’s Encrypt)
|
|
||||||
|
|
||||||
Traffic for **`/.well-known/acme-challenge/*`** must reach **MaksIT.CertsUI** so the HTTP-01 validator can fetch the token body from the API (backed by PostgreSQL). The dedicated route sends that path to the **`server`** service (same `webapiCluster` as `/api`).
|
|
||||||
|
|
||||||
### Kubernetes (Helm)
|
|
||||||
|
|
||||||
The chart can mount **`config.js`** from a ConfigMap (`certsClientRuntime.apiUrl`). Defaults in `values.yaml` may use a full origin (example host); you can also use a **relative** API base such as **`/api`** so the browser uses the same host and port as the page (ingress / port-forward to **8080** on the reverse-proxy Service) without hard-coding `localhost`. Use a full URL only if the UI and API are served from different origins.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Automation and clients
|
|
||||||
|
|
||||||
- **Base URL** for scripts, the Agent, or any HTTP client talking to the **composed** stack should be the **proxy origin**: `http://localhost:8080` when you use Compose’s published port.
|
|
||||||
- **Path shape:** Call **`/api/...`** on that origin (either concatenate `BaseAddress` + `api/...` or set `VITE_API_URL` / runtime `API_URL` to `http://localhost:8080/api` so the client already includes `/api`).
|
|
||||||
- **YARP** forwards request headers to the WebAPI by default (including **`Authorization: Bearer`** for JWT). No special transform is required unless you customize YARP transforms.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Direct vs proxied ports (local dev)
|
|
||||||
|
|
||||||
| Scenario | Typical base URL for Certs HTTP API |
|
|
||||||
|----------|-------------------------------------|
|
|
||||||
| Docker Compose (this repo) | `http://localhost:8080` (through YARP) |
|
|
||||||
| Run **MaksIT.CertsUI** only (F5 / `dotnet run`) | See `launchSettings.json` (e.g. `http://localhost:5016`) — **no** YARP |
|
|
||||||
| Run **ReverseProxy** only (outside Compose) | `launchSettings`: e.g. `http://localhost:5276` — cluster addresses in `appsettings.json` must resolve (Compose service names only work **inside** the Compose network) |
|
|
||||||
|
|
||||||
If authentication succeeds but API calls fail, confirm traffic reaches the **same** WebAPI instance and data volume you expect (not a different port or stale container).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Related docs
|
|
||||||
|
|
||||||
- [User and API key RBAC](./USER_AND_API_KEY_RBAC.md) · [RBAC reference](./RBAC_REFERENCE.md) · [LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md](./LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md)
|
|
||||||
|
|
||||||
## Related files
|
|
||||||
|
|
||||||
- [`src/docker-compose.yml`](../../src/docker-compose.yml), [`src/docker-compose.override.yml`](../../src/docker-compose.override.yml)
|
|
||||||
- [`src/ReverseProxy/Program.cs`](../../src/ReverseProxy/Program.cs)
|
|
||||||
- WebUI runtime API base: [`src/MaksIT.WebUI/public/config.js`](../../src/MaksIT.WebUI/public/config.js), Helm `certsClientRuntime.apiUrl` in [`src/helm/values.yaml`](../../src/helm/values.yaml)
|
|
||||||
@ -1,149 +0,0 @@
|
|||||||
# User (JWT) and API Key RBAC
|
|
||||||
|
|
||||||
This document explains how **role-based access control** differs when the caller is a **logged-in user** (JWT) versus an **API key**, how those principals are loaded, and where the rules live in code.
|
|
||||||
|
|
||||||
**Audience:** Backend developers, security reviewers, and anyone automating against the CertsUI API (WebUI, PowerShell module, `MaksIT.CertsUI.Client`).
|
|
||||||
|
|
||||||
**Related:** [Login and refresh token architecture](./LOGIN_AND_REFRESH_TOKEN_ARCHITECTURE.md) (how JWTs are issued). [RBAC reference](./RBAC_REFERENCE.md) has scope flags and permission matrices. **This** file covers JWT vs API key principals, routes, `GetActingJwtTokenData`, and code references.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Two authentication mechanisms
|
|
||||||
|
|
||||||
| Mechanism | Header | Typical use |
|
|
||||||
|-----------|--------|-------------|
|
|
||||||
| **User (JWT)** | `Authorization: Bearer <access_token>` | Browser (WebUI), interactive tools, services that can refresh tokens |
|
|
||||||
| **API key** | `X-API-KEY: <secret>` | Scripts, CI, PowerShell (`Connect-CertsUI`), `MaksIT.CertsUI.Client`; long-lived secret shown once at creation |
|
|
||||||
|
|
||||||
**Request resolution:** `CertsUIAuthorizationFilter` (`src/MaksIT.CertsUI/Authorization/Filters/CertsUIAuthorizationFilter.cs`) checks **`X-API-KEY` first**. If that header is absent or invalid, it falls back to **Bearer JWT**. If neither yields a valid principal, the request gets **403 Forbidden** (`Authorization required`).
|
|
||||||
|
|
||||||
**Unlike MaksIT.Vault:** Vault uses **`JwtAuthorizationFilter`** on `/api/identity/...` and `/api/apikey/...` (JWT only) and **`VaultAuthorizationFilter`** on `/api/vault/...` (JWT or API key). In **CertsUI**, every protected controller action listed below uses **`CertsUIAuthorizationFilter`** — the same dual path for **identity**, **API keys**, **accounts**, **ACME flow**, and **cache**.
|
|
||||||
|
|
||||||
| Route family | Filter | JWT | `X-API-KEY` |
|
|
||||||
|--------------|--------|-----|-------------|
|
|
||||||
| `/api/identity/...` (except login / refresh) | `CertsUIAuthorizationFilter` | Yes | Yes |
|
|
||||||
| `/api/apikey/...` | `CertsUIAuthorizationFilter` | Yes | Yes |
|
|
||||||
| `/api/account/...`, `/api/certs/...`, `/api/cache/...`, `/api/agent/...`, `/api/debug/...` | `CertsUIAuthorizationFilter` | Yes | Yes |
|
|
||||||
|
|
||||||
**Exceptions (no JWT / API key on the action):**
|
|
||||||
|
|
||||||
- `POST /api/identity/login` and `POST /api/identity/refresh` — anonymous.
|
|
||||||
- `GET /.well-known/acme-challenge/{token}` (`WellKnownController`) — no authorization filter; public HTTP-01 challenge responses (see [REVERSE_PROXY_ROUTING.md](./REVERSE_PROXY_ROUTING.md)).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Two principal types (what RBAC sees)
|
|
||||||
|
|
||||||
After authentication, the filter stores **`CertsUIAuthorizationData`** on `HttpContext` (`HttpContextValue.CertsUIAuthorizationData`).
|
|
||||||
|
|
||||||
| Principal | Type | Where it comes from |
|
|
||||||
|-----------|------|---------------------|
|
|
||||||
| **User** | `JwtTokenData` | Validated JWT; includes `UserId`, `IsGlobalAdmin`, and **entity scopes** from the user record |
|
|
||||||
| **API key** | `ApiKeyData` | Key row by secret + **`ReadApiKeyAuthorization`**; includes `ApiKeyId`, `IsGlobalAdmin` **for that key**, and **entity scopes** stored for the key |
|
|
||||||
|
|
||||||
Services that manage users or API keys do not take `CertsUIAuthorizationData` directly on every method. Controllers call **`GetActingJwtTokenData()`**, which:
|
|
||||||
|
|
||||||
- Returns the real **`JwtTokenData`** for Bearer sessions.
|
|
||||||
- Maps an API key to a **synthetic** `JwtTokenData` (`UserId = Guid.Empty`, `Username = apikey:{id}`, scopes and `IsGlobalAdmin` from the key).
|
|
||||||
|
|
||||||
So identity and API-key **admin** code paths are written against **`JwtTokenData`**, but the acting principal may be a **user** or an **API key** after mapping.
|
|
||||||
|
|
||||||
### 2.1 Global administrator: user vs key (easy to confuse)
|
|
||||||
|
|
||||||
- **`JwtTokenData.IsGlobalAdmin`** — the **signed-in user** is a global administrator (or the key was mapped with this flag).
|
|
||||||
- **`ApiKeyData.IsGlobalAdmin`** — **this API key** was created (or patched) with the global-administrator flag.
|
|
||||||
|
|
||||||
These are **independent**. A global-admin **user** who creates an API key does **not** automatically create a global-admin key unless the create request sets **`IsGlobalAdmin: true`** on the key (UI: separate checkbox). Automation that must bypass scoped limits on **accounts / ACME** needs a **global-admin API key**, not merely a key created by an admin user.
|
|
||||||
|
|
||||||
**Who may set `IsGlobalAdmin` on a key?** Only a principal that is already a **global administrator** after `GetActingJwtTokenData()` may **create** or **patch** a user or API key with `IsGlobalAdmin` enabled (`RbacHelpers.EnsureActorMayAssignGlobalAdmin` / `EnsureActorMayPatchGlobalAdminFlag` in `IdentityService` / `ApiKeyService`).
|
|
||||||
|
|
||||||
### 2.2 Loading API key authorization
|
|
||||||
|
|
||||||
For API key requests, `IsGlobalAdmin` and scopes come from persisted authorization data. If **`ReadApiKeyAuthorization`** fails after the key itself was found, the filter **propagates that failure** (it does **not** continue with `IsGlobalAdmin: false`), so misconfiguration or storage errors are not silently downgraded to “non-admin key” behavior.
|
|
||||||
|
|
||||||
Expired keys (`ExpiresAt <= UtcNow`) are rejected in the filter before authorization is loaded.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Shared RBAC helpers (`ServiceBase`)
|
|
||||||
|
|
||||||
App services inherit from `ServiceBase` (`src/MaksIT.CertsUI/Abstractions/Services/ServiceBase.cs`), which implements:
|
|
||||||
|
|
||||||
- **`RBACWrapper`** — If the request used JWT, runs **`RBACWrapperJwtToken`** with `userRules`; if API key, runs **`RBACWrapperApiKey`** with `apiKeyRules`.
|
|
||||||
- **`RBACWrapperJwtToken` / `RBACWrapperApiKey`** — If `IsGlobalAdmin` is **true** for that principal, return **success immediately** (full access for that wrapper). Otherwise run the supplied rules delegate, or **403** if no rules were supplied.
|
|
||||||
|
|
||||||
So: **`userRules` / `apiKeyRules` = null** means **only global administrators** (JWT or API key, respectively) pass. Non-admins always get **forbidden** with no per-scope check.
|
|
||||||
|
|
||||||
Variants **`RBACWrapper<T>`** / **`RBACWrapperJwtToken<T>`** / **`RBACWrapperApiKey<T>`** follow the same pattern but carry a resource through the rules.
|
|
||||||
|
|
||||||
**Identity / API key services** use **`RBACWrapperJwtToken`** only (after `GetActingJwtTokenData()`), including when the caller used an API key.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Example: accounts and ACME (`AccountService`, `CertsFlowService`)
|
|
||||||
|
|
||||||
Illustrates **authentication without scoped RBAC** today.
|
|
||||||
|
|
||||||
| Action | RBAC pattern | Non–global-admin JWT | Non–global-admin API key |
|
|
||||||
|--------|----------------|----------------------|---------------------------|
|
|
||||||
| **List / read / create / patch / delete account** | `RBACWrapper(..., _ => Ok(), _ => Ok())` | Allowed if authenticated | Allowed if authenticated |
|
|
||||||
| **Certs flow steps** (`CertsFlowService`) | Same open rules | Allowed if authenticated | Allowed if authenticated |
|
|
||||||
| **Registration cache** (`CacheService`) | Same | Allowed if authenticated | Allowed if authenticated |
|
|
||||||
|
|
||||||
There is **no** organization- or account-level scope check on certificate operations yet. **Do not** treat API keys as least-privilege for ACME unless you issue **non-global** keys and accept that they can still drive any account operation once authenticated. Scoped **Identity** / **ApiKey** flags apply to **user and key administration** (see [RBAC reference](./RBAC_REFERENCE.md) §4–§6 and §5.1 for search vs CRUD).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Identity and API key administration (`GetActingJwtTokenData`)
|
|
||||||
|
|
||||||
Operations in **`IIdentityService`** / **`IApiKeyService`** and their controllers take **`JwtTokenData`** from **`GetActingJwtTokenData()`** — so **both** Bearer users and **`X-API-KEY`** callers can hit `/api/identity/...` and `/api/apikey/...` when the key’s scopes (or global admin) allow it.
|
|
||||||
|
|
||||||
Summary (matrices and enforcement detail: [RBAC reference](./RBAC_REFERENCE.md)):
|
|
||||||
|
|
||||||
| Operation | Global-admin actor | Non–global-admin actor |
|
|
||||||
|-----------|-------------------|-------------------------|
|
|
||||||
| **Search users / keys / scopes** | Full list (plus request filters) | Results limited to targets whose **organization** ids are covered by actor **`Identity`** / **`ApiKey`** **`Read`** scopes (enforced in query predicates) |
|
|
||||||
| **Read / patch / delete by id** | Full access | **Intended:** same org coverage as Vault-style identity rules; **current code:** non-admin CRUD wrappers return success without scope checks — see [RBAC reference](./RBAC_REFERENCE.md) §4.1 |
|
|
||||||
| **Create** | Any user/key; may set **`IsGlobalAdmin`** on new principal | **Intended:** `Create` on scopes for all orgs in request; **current code:** non-admin create wrapper allows any authenticated actor |
|
|
||||||
| **Patch `IsGlobalAdmin`** | Allowed | **Forbidden** (`RbacHelpers`) |
|
|
||||||
|
|
||||||
E2E regression tests in `src/e2e-tests/scenarios/Scenario-05-IdentityConfigurations.ps1` assert non-global users cannot create global-admin users or keys.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Troubleshooting
|
|
||||||
|
|
||||||
| Symptom | Likely cause |
|
|
||||||
|---------|----------------|
|
|
||||||
| **403** `Authorization required` | Neither valid `X-API-KEY` nor Bearer JWT. |
|
|
||||||
| **403** `Invalid API Key` / `API Key expired` | Wrong secret, revoked row, or past `ExpiresAt`. |
|
|
||||||
| **403** `User does not have access to resource.` | JWT user is not global-admin and the service used `RBACWrapper` with **null** rules (not used on account/certs today). |
|
|
||||||
| **403** `ApiKey does not have access to resource.` | Same for API key + null rules. |
|
|
||||||
| **403** on **`IsGlobalAdmin`** patch/create | Acting principal is not global-admin (`RbacHelpers`). |
|
|
||||||
| User/key **visible in UI search** but **403 on GET by id** | Unlikely today (CRUD is permissive for non-admins); if scope checks are added to match Vault, search and GET will align — until then, prefer **search** for scoped operators. |
|
|
||||||
| Automation works in UI but **403** with API key | Key lacks **`IsGlobalAdmin`** or scopes; UI user may be global-admin while the key is not. |
|
|
||||||
| **403** / unexpected status right after API key validation | Authorization row read failed; check logs and DB for that `ApiKeyId`. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Code map
|
|
||||||
|
|
||||||
| Topic | Location |
|
|
||||||
|-------|----------|
|
|
||||||
| API key vs JWT resolution | `MaksIT.CertsUI/Authorization/Filters/CertsUIAuthorizationFilter.cs` |
|
|
||||||
| Acting principal for identity/API key | `MaksIT.CertsUI/Authorization/Extensions/HttpContextExtension.cs` (`GetActingJwtTokenData`, `ToActingJwtTokenData`) |
|
|
||||||
| Identity (dual auth) | `MaksIT.CertsUI/Controllers/IdentityController.cs` |
|
|
||||||
| API keys (dual auth) | `MaksIT.CertsUI/Controllers/APIKeyController.cs` |
|
|
||||||
| Accounts / certs / cache (dual auth) | `AccountController`, `CertsFlowController`, `CacheController` |
|
|
||||||
| HTTP-01 (no RBAC) | `MaksIT.CertsUI/Controllers/WellKnownController.cs` |
|
|
||||||
| `RBACWrapper*` helpers | `MaksIT.CertsUI/Abstractions/Services/ServiceBase.cs` |
|
|
||||||
| Scope helpers | `MaksIT.CertsUI/Services/Helpers/RbacHelpers.cs` |
|
|
||||||
| Identity RBAC | `MaksIT.CertsUI/Services/IdentityService.cs` |
|
|
||||||
| API key RBAC | `MaksIT.CertsUI/Services/ApiKeyService.cs` |
|
|
||||||
| Open certs RBAC | `AccountService`, `CertsFlowService`, `CacheService`, `AgentService` |
|
|
||||||
| Principal types | `MaksIT.CertsUI/Authorization/` (`JwtTokenData`, `ApiKeyData`, `CertsUIAuthorizationData`, `IdentityScopeData`) |
|
|
||||||
| Scope enums | `MaksIT.CertsUI.Engine/ScopeEntityType.cs`, `ScopePermission.cs` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: May 2026*
|
|
||||||
@ -1,5 +1,15 @@
|
|||||||
# MaksIT.CertsUI.Client.PowerShell
|
# MaksIT.CertsUI.Client.PowerShell
|
||||||
|
|
||||||
C# binary module with custom cmdlets for the CertsUI HTTP API.
|
PowerShell module for the CertsUI HTTP API (built on **MaksIT.CertsUI.Client**).
|
||||||
|
|
||||||
**Documentation:** [PowerShell client module](../../assets/docs/POWERSHELL_CLIENT_MODULE.md)
|
**Requirements:** PowerShell 7 on **.NET 10**. **Cmdlet help:** `Get-Help Connect-CertsUI -Full` (and other exported commands) after `Import-Module`. **E2E:** [src/e2e-tests/README.md](../e2e-tests/README.md).
|
||||||
|
|
||||||
|
**Quick start:**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Import-Module .\bin\Debug\net10.0\MaksIT.CertsUI.Client.PowerShell.psd1 -Force
|
||||||
|
Connect-CertsUI -BaseAddress "http://localhost:8080" -ApiKey "<key>"
|
||||||
|
Test-CertsUIHealth
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires **PowerShell 7** with a **.NET 10** host.
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MaksIT.Core" Version="1.6.5" />
|
<PackageReference Include="MaksIT.Core" Version="1.6.7" />
|
||||||
<ProjectReference Include="..\MaksIT.CertsUI.Client\MaksIT.CertsUI.Client.csproj" />
|
<ProjectReference Include="..\MaksIT.CertsUI.Client\MaksIT.CertsUI.Client.csproj" />
|
||||||
<ProjectReference Include="..\MaksIT.CertsUI.Contracts\MaksIT.CertsUI.Contracts.csproj" />
|
<ProjectReference Include="..\MaksIT.CertsUI.Contracts\MaksIT.CertsUI.Contracts.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -14,9 +14,9 @@
|
|||||||
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="8.0.1" />
|
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="8.0.1" />
|
||||||
<PackageReference Include="linq2db" Version="6.3.0" />
|
<PackageReference Include="linq2db" Version="6.3.0" />
|
||||||
<PackageReference Include="linq2db.PostgreSQL" Version="6.3.0" />
|
<PackageReference Include="linq2db.PostgreSQL" Version="6.3.0" />
|
||||||
<PackageReference Include="MaksIT.Core" Version="1.6.5" />
|
<PackageReference Include="MaksIT.Core" Version="1.6.7" />
|
||||||
<PackageReference Include="MaksIT.Dapr" Version="2.0.0" />
|
<PackageReference Include="MaksIT.Dapr" Version="2.0.0" />
|
||||||
<PackageReference Include="MaksIT.Results" Version="2.0.1" />
|
<PackageReference Include="MaksIT.Results" Version="2.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DefaultItemExcludes>$(DefaultItemExcludes);Models\obj\**;Models\bin\**</DefaultItemExcludes>
|
<DefaultItemExcludes>$(DefaultItemExcludes);Models\obj\**;Models\bin\**</DefaultItemExcludes>
|
||||||
<Version>3.5.1</Version>
|
<Version>3.5.2</Version>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||||
@ -10,9 +10,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MaksIT.Core" Version="1.6.5" />
|
<PackageReference Include="MaksIT.Core" Version="1.6.7" />
|
||||||
<PackageReference Include="MaksIT.Dapr" Version="2.0.0" />
|
<PackageReference Include="MaksIT.Dapr" Version="2.0.0" />
|
||||||
<PackageReference Include="MaksIT.Results" Version="2.0.1" />
|
<PackageReference Include="MaksIT.Results" Version="2.0.2" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -1,89 +1,28 @@
|
|||||||
# CertsUI API key E2E (PowerShell)
|
# CertsUI E2E tests (PowerShell)
|
||||||
|
|
||||||
End-to-end tests run against a **running** CertsUI deployment via [`MaksIT.CertsUI.Client.PowerShell`](../MaksIT.CertsUI.Client.PowerShell/) cmdlets. They are **not** part of CI (`utils/engines/test/scriptSettings.json`); run them manually after compose, Helm, or k3s deploy.
|
API-key end-to-end tests against a running CertsUI deployment via **MaksIT.CertsUI.Client.PowerShell**.
|
||||||
|
|
||||||
Cmdlet reference and `Import-Module` paths: [assets/docs/POWERSHELL_CLIENT_MODULE.md](../../assets/docs/POWERSHELL_CLIENT_MODULE.md).
|
**Module quick start:** [MaksIT.CertsUI.Client.PowerShell/README.md](../MaksIT.CertsUI.Client.PowerShell/README.md).
|
||||||
|
|
||||||
Requires **latest PowerShell 7** with a **.NET 10** host (install from [PowerShell releases](https://github.com/PowerShell/PowerShell/releases)). **pwsh 7.4 / .NET 8** cannot load the `net10.0` module. Verify: `[System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription` → **.NET 10.x**.
|
|
||||||
|
|
||||||
**Docker Compose + YARP on `http://localhost:8080`:** use `http://localhost:8080` as the base URL (no `/api` suffix).
|
|
||||||
|
|
||||||
## Credentials
|
## Credentials
|
||||||
|
|
||||||
Uses **one** environment variable: **`CERTSUI_E2E_CREDENTIALS`** — UTF-8 text, Base64-encoded.
|
Set **`CERTSUI_E2E_CREDENTIALS`** — Base64 of UTF-8 `<baseUrl><US><apiKey>` (`<US>` = ASCII unit separator, `[char]0x1F`):
|
||||||
|
|
||||||
The script reads it with **`[Environment]::GetEnvironmentVariable`** in order **Process**, **User**, then **Machine** (same as Vault E2E).
|
|
||||||
|
|
||||||
After Base64 decode, the payload is a **single line**: `<baseUrl><US><apiKey>` where **`<US>`** is ASCII Unit Separator, U+001F (`[char]0x1F`).
|
|
||||||
|
|
||||||
Encode in pwsh:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$us = [char]0x1F # required on its own line before $b64
|
|
||||||
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("http://localhost:8080$us" + 'paste-raw-api-key-here'))
|
|
||||||
$b64
|
|
||||||
```
|
|
||||||
|
|
||||||
Persist for your **user** account:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
[Environment]::SetEnvironmentVariable('CERTSUI_E2E_CREDENTIALS', '<paste-base64-here>', 'User')
|
|
||||||
```
|
|
||||||
|
|
||||||
Or only for the **current process**:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:CERTSUI_E2E_CREDENTIALS = '<paste-base64-here>'
|
|
||||||
```
|
|
||||||
|
|
||||||
### JWT credentials (optional)
|
|
||||||
|
|
||||||
Identity admin scenarios use the **global admin API key** from `CERTSUI_E2E_CREDENTIALS` (`X-API-KEY`) when the server supports it. Optionally set **`CERTSUI_E2E_JWT_CREDENTIALS`** — same encoding, payload `<adminUsername><US><password>` — for JWT-only probes (e.g. scoped-user login).
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$us = [char]0x1F
|
$us = [char]0x1F
|
||||||
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("admin$us" + 'your-admin-password'))
|
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("http://localhost:8080$us" + 'your-api-key'))
|
||||||
[Environment]::SetEnvironmentVariable('CERTSUI_E2E_JWT_CREDENTIALS', $b64, 'User')
|
[Environment]::SetEnvironmentVariable('CERTSUI_E2E_CREDENTIALS', $b64, 'User')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional **`CERTSUI_E2E_JWT_CREDENTIALS`** — same encoding, payload `<adminUsername><US><password>` — for JWT-only identity probes.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
pwsh -File .\src\e2e-tests\Test-CertsUiApiKeyE2E.ps1
|
pwsh -File .\src\e2e-tests\Test-CertsUiApiKeyE2E.ps1
|
||||||
```
|
|
||||||
|
|
||||||
Or run `src\e2e-tests\Test-CertsUiApiKeyE2E.bat` after credentials are set.
|
|
||||||
|
|
||||||
Filter scenarios:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
pwsh -File .\src\e2e-tests\Test-CertsUiApiKeyE2E.ps1 -Scenario Health
|
pwsh -File .\src\e2e-tests\Test-CertsUiApiKeyE2E.ps1 -Scenario Health
|
||||||
pwsh -File .\src\e2e-tests\Test-CertsUiApiKeyE2E.ps1 -Scenario AccountReadPatch
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment variables
|
Or `src\e2e-tests\Test-CertsUiApiKeyE2E.bat` after credentials are set.
|
||||||
|
|
||||||
| Variable | Purpose |
|
E2E is **not** run in CI. Requires **PowerShell 7** on **.NET 10** (see script error text if the host is wrong).
|
||||||
|----------|---------|
|
|
||||||
| `CERTSUI_E2E_CREDENTIALS` | Required — Base64 `<baseUrl><US><apiKey>` |
|
|
||||||
| `CERTSUI_E2E_JWT_CREDENTIALS` | Optional — Base64 `<adminUser><US><password>` |
|
|
||||||
| `CERTSUI_E2E_EXPECT_MIN_DISTINCT_INSTANCES` | Optional — `MultiReplica` defaults to **1** (Docker Compose); set **2**+ for HA |
|
|
||||||
| `CERTSUI_E2E_ACCOUNT_ID` | Optional — `AccountReadPatch` target account (else first account) |
|
|
||||||
|
|
||||||
## Registered scenarios
|
|
||||||
|
|
||||||
| Id | Cmdlets |
|
|
||||||
|----|---------|
|
|
||||||
| `Health` | `Test-CertsUIHealth` |
|
|
||||||
| `ApiKeyConcurrentReads` | `Get-CertsUIAccounts` (connected session) |
|
|
||||||
| `MultiReplica` | `Get-CertsUIRuntimeInstanceId` (default **1** instance for Docker Compose; set `CERTSUI_E2E_EXPECT_MIN_DISTINCT_INSTANCES=2` for HA) |
|
|
||||||
| `AccountReadPatch` | `Get-CertsUIAccounts`, `Get-CertsUIAccount`, `Invoke-CertsUIPatchAccount` |
|
|
||||||
| `IdentityConfigurations` | Global admin API key: users/API keys (all scope configs), PATCH remove all scopes, global-admin create probe |
|
|
||||||
|
|
||||||
`AccountReadPatch` skips when there are no accounts.
|
|
||||||
|
|
||||||
**Docker Compose:** no extra env needed for `MultiReplica` (single server container). For k8s with multiple pods: `$env:CERTSUI_E2E_EXPECT_MIN_DISTINCT_INSTANCES = '2'`.
|
|
||||||
|
|
||||||
## Unit tests (mock HTTP only)
|
|
||||||
|
|
||||||
[`MaksIT.CertsUI.Client.Tests`](../MaksIT.CertsUI.Client.Tests/) exercises `CertsUIClient` with `FakeHttpMessageHandler` — no live server. Included in CI via `dotnet test` on that project.
|
|
||||||
|
|||||||
@ -48,7 +48,7 @@ Use `existingConfigMap` / `existingSecret` to mount resources created outside th
|
|||||||
|
|
||||||
PostgreSQL: **external** — set **`certsServerSecrets.certsEngineConfiguration.connectionString`** (or mount **`existingSecret`** with the same **`appsecrets.json`** shape). The app waits via in-process **`PostgresStartupWait`** during migrations and bootstrap. Local dev: **`docker-compose`** **`postgres`** service (not part of this chart).
|
PostgreSQL: **external** — set **`certsServerSecrets.certsEngineConfiguration.connectionString`** (or mount **`existingSecret`** with the same **`appsecrets.json`** shape). The app waits via in-process **`PostgresStartupWait`** during migrations and bootstrap. Local dev: **`docker-compose`** **`postgres`** service (not part of this chart).
|
||||||
|
|
||||||
Health while starting: **`GET /health/startup`** (JSON phase snapshot); traffic should use **`GET /health/ready`** (503 until bootstrap completes). See **`assets/docs/HA_ARCHITECTURE.md`**.
|
Health while starting: **`GET /health/startup`** (JSON phase snapshot); traffic should use **`GET /health/ready`** (503 until bootstrap completes). Server **`startupProbe`** defaults to **`/health/ready`** (see chart **`values.yaml`**).
|
||||||
|
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
## Uninstall
|
## Uninstall
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user