Add LDAP Distinguished Name Helper and Credential Audit Context
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implemented LdapDistinguishedNameHelper for escaping RDN and filter values.
- Created AuthorityCredentialAuditContext and IAuthorityCredentialAuditContextAccessor for managing credential audit context.
- Developed StandardCredentialAuditLogger with tests for success, failure, and lockout events.
- Introduced AuthorityAuditSink for persisting audit records with structured logging.
- Added CryptoPro related classes for certificate resolution and signing operations.
This commit is contained in:
master
2025-11-09 12:21:38 +02:00
parent ba4c935182
commit 75c2bcafce
385 changed files with 7354 additions and 7344 deletions

View File

@@ -131,6 +131,55 @@ Standard plug-in installs begin with an operator-provided manifest and secrets b
_Source:_ `docs/assets/authority/authority-plugin-bootstrap-sequence.mmd`
### 6.2 Credential audit telemetry (SEC2/SEC3)
- Password verification now emits `authority.plugin.standard.password_verification` records through the shared `IAuthEventSink`. `StandardCredentialAuditLogger` converts every outcome (success, lockout, password reset, MFA requirement) into `AuthEventRecord` instances so `/token` observability can be correlated with plugin activity.
- `IAuthorityCredentialAuditContextAccessor` captures the callers correlation ID, client ID, tenant, remote IP, forwarded addresses, and user agent. OpenIddict handlers push a scope right before invoking the plug-in, and the logger automatically copies those fields into the audit event:
```csharp
using var scope = auditContextAccessor.BeginScope(new AuthorityCredentialAuditContext(
correlationId,
clientId,
tenantId,
rateLimiterMetadata?.RemoteIp,
rateLimiterMetadata?.ForwardedFor,
rateLimiterMetadata?.UserAgent));
```
- Outcome mapping is deterministic: `AuthorityCredentialFailureCode.LockedOut` ⇒ `AuthEventOutcome.LockedOut`, `RequiresPasswordReset`/`PasswordExpired` ⇒ `RequiresFreshAuth`, and `RequiresMfa` ⇒ `RequiresMfa`. Anything else falls back to `Failure`.
- Lockout/rate-limit telemetry is carried via structured properties so SOC dashboards can slice the data:
- `plugin.failed_attempts` running count prior to the current decision.
- `plugin.failed_attempts_cleared` how many failures were cleared after a successful login.
- `plugin.lockout_until` ISO8601 timestamp showing when the account unlocks (classified as `Personal`).
- `plugin.retry_after_seconds` ceiling of `AuthorityCredentialVerificationResult.RetryAfter.TotalSeconds`; surfaced on both the audit event and the verification result to guide HTTP 429/423 responses.
- `plugin.rehashed` algorithm tag (`argon2id`) when a legacy hash is upgraded.
- `plugin.failure_code` enum name corresponding to the failure classification.
- Remember that everything you add to `AuthorityCredentialVerificationResult.AuditProperties` flows into both the `/token` audit event and the plug-inscoped event above, so keep names stable and values deterministic for Offline Kit replay.
- **Mongo2Go prerequisite:** the plugin test suite relies on Mongo2Gos embedded `mongod`. Export the OpenSSL 1.1 shim described in `docs/19_TEST_SUITE_OVERVIEW.md` (section “Mongo2Go / OpenSSL shim”) before running `dotnet test`, e.g.:
```bash
export LD_LIBRARY_PATH=\"$(pwd)/tests/native/openssl-1.1/linux-x64:${LD_LIBRARY_PATH:-}\"
```
Without this step the embedded server fails to launch on OpenSSL 3 systems, causing timeouts in `StandardUserCredentialStoreTests`.
### 6.3 Plugin-specific mitigations (SEC5.PLG)
- Bootstrap seed users default to `RequirePasswordReset = true`. `StandardUserCredentialStore.EnsureBootstrapUserAsync` enforces the configured password policy, rejects partial credentials, and emits `authority.plugin.load` telemetry so operators can prove who seeded the initial principals.
- Password policy overrides are validated against a built-in baseline (min length 12 + mixed character classes). The registrar now logs a structured warning whenever a deployment attempts to weaken those defaults, giving security reviewers an audit breadcrumb and satisfying the SEC5.PLG threat-model requirement.
- All bootstrap and password operations use `ICryptoProvider` + Argon2id; legacy PBKDF2 hashes are upgraded inline and tagged via `plugin.rehashed`. Document any deviations so downstream plug-ins (or auditors) can reason about entropy expectations.
- Lockout metadata is deterministic: `plugin.lockout_until` + `plugin.retry_after_seconds` form the authoritative signal for incident response, and their presence is now noted in the Authority threat model (`docs/security/authority-threat-model.md`).
- When extending the Standard plug-in (or authoring a new one), keep these mitigations intact: enforce baseline policies, require explicit password reset flags on bootstrap flows, and emit the audit properties listed above. Thirdparty plugins are expected to follow the same contract before they can advertise `SupportsPassword` or `SupportsBootstrap`.
### 6.4 LDAP plug-in quick reference (PLG7.IMPL-005)
- **Mutual TLS & trust stores.** `security.requireTls=true` enforces LDAPS/startTLS; set `security.requireClientCertificate=true` to demand mutual TLS. When that flag is enabled you must supply `connection.clientCertificate.pfxPath` + `passwordSecret`. Bundle CA chains under `connection.trustStore.bundlePath` and keep the files inside Offline Kit paths (`plugins/authority/ldap/**`) so air-gapped installs can import them without editing manifests.
- **DNtorole mapping.** `claims.groupToRoleMap` is ideal for static DNs (e.g. `cn=stellaops-admins,...` → `operators`). Regex mappings let you project portions of the DN into role names: define `pattern` with named captures (`(?P<role>...)`) and use `{role}` placeholders in `roleFormat`. The enricher sorts all emitted roles, dedupes, and adds them as `ClaimTypes.Role`.
- **Attribute pass-through.** `claims.extraAttributes` pairs the outgoing claim name with the LDAP attribute to read (first value wins). Only non-empty strings are written, which keeps audit/compliance data deterministic.
- **Mongo claims cache.** `claims.cache.enabled=true` wires the `MongoLdapClaimsCache` (default collection `ldap_claims_cache_<pluginName>`). Set `ttlSeconds` according to your directory freshness SLA and adjust `maxEntries` to cap disk usage; eviction is deterministic (oldest entries removed first). Offline Kit bundles now include the collection name requirements so replicas can pre-create capped collections.
- **Client provisioning audit mirror.** `clientProvisioning.auditMirror.enabled=true` persists every LDAP write into Mongo (`ldap_client_provisioning_<plugin>` by default) with `{operation, dn, tenant, project, secretHash}`. That mirror is shipped in Offline Kits so regulators can diff LDAP state even without directory access. When `clientProvisioning.enabled=false`, the registrar logs a warning and downgrades the capability at runtime.
- **Sample manifest + binaries.** The curated manifest lives at `etc/authority.plugins/ldap.yaml` and demonstrates TLS, regex mappings, caching, and audit mirror options. Offline Kits copy both the manifest and the compiled plug-in into `plugins/authority/StellaOps.Authority.Plugin.Ldap/` so operators can drop them straight into air-gapped composer deployments.
## 7. Configuration & Secrets
- Authority looks for manifests under `etc/authority.plugins/`. Each YAML file maps directly to a plug-in name.
- Support environment overrides using `STELLAOPS_AUTHORITY_PLUGINS__DESCRIPTORS__<NAME>__...`.
@@ -154,15 +203,33 @@ _Source:_ `docs/assets/authority/authority-plugin-bootstrap-sequence.mmd`
- Token scopes should be normalised (trimmed, unique, ordinal sort) before returning from plug-in verification paths. `TokenPersistenceHandlers` will keep that ordering for downstream consumers.
### 7.2 Claims & Enrichment Checklist
- Authority always sets the OpenID Connect basics: `sub`, `client_id`, `preferred_username`, optional `name`, and `role` (for password flows). Plug-ins must use `IClaimsEnricher` to append additional claims in a **deterministic** order (sort arrays, normalise casing) so resource servers can rely on stable shapes.
- Recommended enrichment keys:
- `stellaops.realm` plug-in/tenant identifier so services can scope policies.
- `stellaops.subject.type` values such as `human`, `service`, `bootstrap`.
- `groups` / `projects` sorted arrays describing operator entitlements.
- Claims visible in tokens should mirror what `/token` and `/userinfo` emit. Avoid injecting sensitive PII directly; mark values with `ClassifiedString.Personal` inside the plug-in so audit sinks can tag them appropriately.
- For client-credential flows, remember to enrich both the client principal and the validation path (`TokenValidationHandlers`) so refresh flows keep the same metadata.
### 7.3 Revocation Bundles & Reasons
- Authority always sets the OpenID Connect basics: `sub`, `client_id`, `preferred_username`, optional `name`, and `role` (for password flows). Plug-ins must use `IClaimsEnricher` to append additional claims in a **deterministic** order (sort arrays, normalise casing) so resource servers can rely on stable shapes.
### Claims enrichment & caching contracts
LDAP/AD plug-ins now expose first-class `claims.*` configuration to keep enrichment consistent:
- `claims.groupAttribute`, `claims.groupToRoleMap`, and `claims.regexMappings` translate directory DNs into Authority roles. Regex mappings honour both .NET-style `(?<role>)` and Python-style `(?P<role>)` capture syntax; names become `{role}` placeholders inside `roleFormat`.
- `claims.extraAttributes` is a deterministic map of `{ claimName: ldapAttribute }`. Only the first attribute value is propagated and plug-ins must skip null/empty payloads.
- `claims.cache.*` enables a Mongo-backed cache (`ldap_claims_cache_<pluginName>` by default) with TTL + capacity trims so repeated password grants avoid hammering the directory. TTL must be > 0 seconds and max entries ≥ 0. Collection names are normalised to lowercase ASCII and strip `/`, `\`, and `:` to remain Offline-Kit friendly.
When the cache is disabled, plug-ins inject `DisabledLdapClaimsCache` so the enricher path stays free of null checks. Cache documents must stay tenant-scoped and include `cachedAt`/`expiresAt` so operators can audit freshness. See `StellaOps.Authority.Plugin.Ldap.Claims` for the reference implementation.
- Recommended enrichment keys:
- `stellaops.realm` plug-in/tenant identifier so services can scope policies.
- `stellaops.subject.type` values such as `human`, `service`, `bootstrap`.
- `groups` / `projects` sorted arrays describing operator entitlements.
- Claims visible in tokens should mirror what `/token` and `/userinfo` emit. Avoid injecting sensitive PII directly; mark values with `ClassifiedString.Personal` inside the plug-in so audit sinks can tag them appropriately.
- For client-credential flows, remember to enrich both the client principal and the validation path (`TokenValidationHandlers`) so refresh flows keep the same metadata.
### Client provisioning & audit mirror
- `clientProvisioning.enabled` must be true for the LDAP plug-in to expose `IClientProvisioningStore` and advertise the `clientProvisioning` capability. If the manifest lists the capability but the config disables it, startup logs a warning and the capability stays off.
- `clientProvisioning.containerDn` is the base DN for machine/service accounts; the plug-in automatically builds RDNs as `<rdnAttribute>=<clientId>` (default `cn`) and escapes special characters to remain RFC 4514 compliant.
- `clientProvisioning.secretAttribute` controls which LDAP attribute stores the client secret; the run-time writes the cleartext secret you pass during provisioning, while Mongo keeps only the hashed reference for audit (`AuthoritySecretHasher`).
- `clientProvisioning.auditMirror.*` persists deterministic Mongo documents (default collection `ldap_client_provisioning_<plugin>`) capturing `{operation, dn, tenant, project, secretHash}` so operators can diff LDAP state even in air-gaps.
- LDAP writes bind with the configured service account (`connection.bindDn` + secret). If the account loses modify permissions the store returns `ldap_error` and no Mongo state is changed, giving operators a single place to investigate.
### 7.3 Revocation Bundles & Reasons
- Use `IAuthorityRevocationStore` to record subject/client/token revocations when credentials are deleted or rotated. Stick to the standard categories (`token`, `subject`, `client`, `key`).
- Include a deterministic `reason` string and optional `reasonDescription` so operators understand *why* a subject was revoked when inspecting bundles offline.
- Plug-ins should populate `metadata` with stable keys (e.g., `revokedBy`, `sourcePlugin`, `ticketId`) to simplify SOC correlation. The keys must be lowercase, ASCII, and free of secrets—bundles are mirrored to air-gapped agents.