diff --git a/.github/workflows/cd-publish-nuget-prerelease.yml b/.github/workflows/cd-publish-nuget-prerelease.yml index 36c0dcf3..18b01344 100644 --- a/.github/workflows/cd-publish-nuget-prerelease.yml +++ b/.github/workflows/cd-publish-nuget-prerelease.yml @@ -9,7 +9,7 @@ name: CD - Publish NuGet Prerelease # Asymmetric with `cd-publish-staging-image.yml`, which DOES auto- # publish a Docker image on every develop merge. The reason: # -# - Docker `:staging` is a moving pointer tag. Each push +# - Docker `:beta` is a moving pointer tag. Each push # overwrites the previous one; the previous image becomes # untagged and the GHCR retention rule deletes it. Cheap to # update on every commit. diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index 5f7f7806..8e6b02f7 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -29,8 +29,8 @@ name: CD - Release # value — set a GHCR retention rule on `candidate-*` if cleanup matters. # # Sibling workflows for staging / prerelease / editorial doc paths: -# - cd-publish-staging-image.yml — pushes `:staging` Docker tag -# (auto on develop push + manual) +# - cd-publish-staging-image.yml — pushes `:beta` / `:` moving +# Docker tags (auto on develop push + manual) # - cd-publish-nuget-prerelease.yml — prerelease NuGet for client-lib # (manual workflow_dispatch) # - cd-deploy-docs.yml — editorial doc deploy to Shelf diff --git a/dev-docs/future-features/app-resources-as-permissions.md b/dev-docs/future-features/app-resources-as-permissions.md index 4f118592..7b7b3846 100644 --- a/dev-docs/future-features/app-resources-as-permissions.md +++ b/dev-docs/future-features/app-resources-as-permissions.md @@ -48,7 +48,11 @@ > **Status der ursprünglichen Note:** Idea — captured 2026-05-07, > refined 2026-05-08 with ID-anchored design (string-keyed > `List` rejected because RS-subsets and role-grants would -> drift on rename/delete). Not started. +> drift on rename/delete). **Shipped** — das ID-anchored Modell ist +> umgesetzt (`App.Permissions`-Catalog, `AppPermission.Id`, +> `PermissionRole.PermissionIds`, per-RS-Subset-Filterung, +> Claims-Flattening). Single-Source-of-Truth ist +> [`permission-modell.md`](./permission-modell). > **Why:** While walking a user through OAuth-API setup we hit a > conceptual cliff: the IdP's `App.Resources` field stores only the > middle segment of a permission (`policy`, `knowledge`, `mcp`), diff --git a/dev-docs/future-features/federation-v1-design.md b/dev-docs/future-features/federation-v1-design.md index 17042536..81cc2ee3 100644 --- a/dev-docs/future-features/federation-v1-design.md +++ b/dev-docs/future-features/federation-v1-design.md @@ -1,6 +1,6 @@ # Federation v1 — Implementation Spec -Status: **Design decided (A–G settled, 2026-05-29), ready for an implementation plan. No code yet.** Concretizes the federation model decided in [identity-lifecycle-untangle](./identity-lifecycle-untangle#federation-prior-art) into real code seams. Based on integration map `wf_63933d9f-149`. Depends on Hotfix C (PR #21, `766c9f8`). +Status: **Shipped (spec PR #23 `4fa3af0`, implementation PR #24 `0b70b31`); durable leased-membership = deferred v2.** Login-time in-memory membership derivation, EvalPrincipal/ExternalGroups, the external claims store, and baking federated `resource_access` into the access token (hub-by-default + broker-opt-in) are all live with passing tests. The remaining v1↔v2 gap is the durable enumerable filter (see [v1 vs v2](#v1-vs-v2)). Concretizes the federation model decided in [identity-lifecycle-untangle](./identity-lifecycle-untangle#federation-prior-art) into real code seams. Based on integration map `wf_63933d9f-149`. Depends on Hotfix C (PR #21, `766c9f8`). > **Background** (prior art, the stale-admin trap, the hub-vs-broker spectrum): see the [untangle doc](./identity-lifecycle-untangle). This doc is the build template. diff --git a/dev-docs/future-features/federation-v1-implementation-plan.md b/dev-docs/future-features/federation-v1-implementation-plan.md index a308b7ad..0668e3d0 100644 --- a/dev-docs/future-features/federation-v1-implementation-plan.md +++ b/dev-docs/future-features/federation-v1-implementation-plan.md @@ -1,6 +1,6 @@ # Federation v1 — Implementation Plan -Status: **Build-ready.** Concretizes [federation-v1-design](./federation-v1-design) (decisions A–G settled 2026-05-29) into an ordered, file:line-accurate build sequence. Every seam below was re-verified against the working tree at `dev-docs/federation-v1-design` HEAD `a57ee04` (which includes Hotfix C, PR #21, `766c9f8`) by a 7-agent read-only verification pass (`wf_5ba83c8c-b9e`, raw findings in `.local/`). The design is decided; this document is the **how and in what order**, not a re-litigation of A–G. +Status: **Shipped (Phases 0–5 + v1.1).** Implemented and merged to develop via PR #24 (squash `0b70b31`, 2026-05-29) — the phase breakdown below is the as-built record. Only Phase 6 (per-realm session TTL, decision E) and the v2 slice (durable leased external-membership) remain. Concretizes [federation-v1-design](./federation-v1-design) (decisions A–G settled 2026-05-29) into an ordered, file:line-accurate build sequence. Every seam below was re-verified against the working tree at `dev-docs/federation-v1-design` HEAD `a57ee04` (which includes Hotfix C, PR #21, `766c9f8`) by a 7-agent read-only verification pass (`wf_5ba83c8c-b9e`, raw findings in `.local/`). The design is decided; this document is the **how and in what order**, not a re-litigation of A–G. > Read the [design spec](./federation-v1-design) first for the *why*. This plan assumes the model in one paragraph, the two-layer source filter, and decisions A–G as given. diff --git a/dev-docs/future-features/identity-lifecycle-untangle.md b/dev-docs/future-features/identity-lifecycle-untangle.md index 27eec11a..292a578d 100644 --- a/dev-docs/future-features/identity-lifecycle-untangle.md +++ b/dev-docs/future-features/identity-lifecycle-untangle.md @@ -41,7 +41,7 @@ Status: **mostly implemented** — original analysis/decision-gate pass from 202 - ❌ STILL OPEN (durable-lease, the large piece): replace the flat `Group.MemberIds: List` with a source-attributed external-membership class `(groupId, principalId, source = provider:, grantedAt, leaseExpiresAt, lastReconfirmedAt)`; effective members = union of { manual } + { local-JsEval } + { per-provider external WHERE leaseExpiresAt > now }. Federation v1 shipped only the *session-scoped* derivation ("the session is the lease"); this is the durable, login-independent version. - ❌ STILL OPEN: refresh triggers — login-FORCE SET-reconcile, lease-expiry sweep (Quartz, fail-closed), optional inbound SCIM / scheduled LDAP-or-Graph pull (net-new surface). - ✅ Privilege guardrail (federated/JsEval auto-membership must never confer `realm:admin`) is already enforced by Federation v1 decision G (bidirectional config guard + `ExpandBypassTiers` strip). -- ❌ STILL OPEN: resolve the `docs/concepts/auto-membership.md` `externalClaims`/`OrganizationalUnit`/`Department` doc-vs-code contradiction (the spec of the wanted, unbuilt durable-claims surface). +- ✅ RESOLVED (PR #24): the `docs/concepts/auto-membership.md` `externalClaims`/`OrganizationalUnit`/`Department` doc-vs-code contradiction is closed — the misleading examples were replaced by the shipped `ExternallyDrivable`-group + `p.ExternalGroups` surface (session-scoped federation deriver), and the durable-fields table now explicitly states there is no `OrganizationalUnit`/`Department`/`externalClaims`. (The *durable, login-independent* claims surface remains the Phase-4-large durable-lease piece above.) **Known low-pri follow-up (surfaced by the Phase-3 adversarial review):** diff --git a/dev-docs/future-features/index.md b/dev-docs/future-features/index.md index 269a5959..23e524c2 100644 --- a/dev-docs/future-features/index.md +++ b/dev-docs/future-features/index.md @@ -19,7 +19,7 @@ Severity. Detail-Pages unten. ⭐ **Federation v1 — implementation spec (2026-05-29):** [federation-v1-design](./federation-v1-design) -— concretizes the agreed v1 model into real code seams (one seam: `ExternalLoginProcessor.ProcessAsync`; authz resolved late at token time): the unified claims-per-source store, the two-layer source filter, "session = lease" (mid-session timer rejected), the new per-provider/per-group flags. All design decisions A–G are settled; the doc is the build template. +— concretizes the agreed v1 model into real code seams (one seam: `ExternalLoginProcessor.ProcessAsync`; authz resolved late at token time): the unified claims-per-source store, the two-layer source filter, "session = lease" (mid-session timer rejected), the new per-provider/per-group flags. All design decisions A–G are settled; the doc is the build template. **✅ Shipped (PR #23 spec, PR #24 `0b70b31` broker → session-derived authz + v1.1 token layer).** ### Audit-Followups (in Severity-Reihenfolge) @@ -35,8 +35,8 @@ Severity. Detail-Pages unten. - [SAML federation — implementation plan](./saml-federation) — konkreter Implementation-Plan für den SP-Use-Case (Modgud konsumiert Customer-IdP). Lib-Wahl: ITfoxtec.Identity.Saml2. Status: - Decisions captured 2026-05-27, in active development on - `feat/saml-federation`. + ✅ Shipped (PR #17, `8fc3df0`) — SAML 2.0 SP federation + + login-provider single-modal Add+Edit. - [SAML AMR → `amr` wiring](./saml-amr-wiring) — `SamlFlavorData.AmrMapping` is configured/seeded but parsed-but-not-consumed (federation v1 deferral I15). Captures what the read-side wiring would do and why deferring is @@ -61,7 +61,11 @@ Per-realm theming: logo, colors, brand copy, optional custom CSS. custom copy → custom CSS), with a phased rollout plan that ships the 80%-coverage version first. -**Status:** Design captured 2026-05-07. Not started. +**Status:** ✅ Phase 1 shipped — per-realm token-based branding +(`BrandingSettings`), asset library (BYTEA store, anonymous +`/api/assets/{id}` read), Branding/Pages/PageEditor admin views +(`8c8dea5`, `2ec0e58`, `ae2f9ca`). Page-builder runtime rendering still +deferred; custom-CSS tier not started. ### [Service Account credentials — link to OAuth Clients](./service-account-credentials) @@ -180,7 +184,9 @@ Emission verworfen (UserInfo-Emission stattdessen), Bypass-Tiers auf 2 reduziert, Slug-tagged-Format auf bare reduziert. Diese Note bleibt als Designexploration — Detail-Banner oben in der Note. -**Status:** Note 2026-05-07, teilweise revidiert 2026-05-08. +**Status:** Note 2026-05-07, teilweise revidiert 2026-05-08. ✅ Das +ID-anchored Permission-Modell ist implementiert (siehe +[Permission-Modell](./permission-modell)). ### DCR for MCP clients — ✅ shipped diff --git a/dev-docs/future-features/permission-modell.md b/dev-docs/future-features/permission-modell.md index d21f3f2f..1f9b9adc 100644 --- a/dev-docs/future-features/permission-modell.md +++ b/dev-docs/future-features/permission-modell.md @@ -14,9 +14,11 @@ > Jede **App** deklariert ihre vollständige **Permission-Liste** als > Catalog im strikten Format `:`; jeder **Resource > Server** kriegt davon ein Subset zugewiesen; **`/connect/userinfo` -> emittiert per-Audience-nested Blocks mit Roles, Permissions und -> Groups** (Bypass-Tiers vom IdP vor-expandiert); Konsumenten lesen -> ihren Block über den Audience-Key und machen stumpfes exact-match. +> emittiert per-Audience-nested Blocks mit Roles und Permissions** +> (Bypass-Tiers vom IdP vor-expandiert; **Groups bleiben heute +> IdP-internal und werden nicht emittiert** — siehe §5); Konsumenten +> lesen ihren Block über den Audience-Key und machen stumpfes +> exact-match. Ab hier alles im Detail. diff --git a/dev-docs/future-features/production-readiness-audit-2026-05-13.md b/dev-docs/future-features/production-readiness-audit-2026-05-13.md index a2362eda..13da549e 100644 --- a/dev-docs/future-features/production-readiness-audit-2026-05-13.md +++ b/dev-docs/future-features/production-readiness-audit-2026-05-13.md @@ -26,7 +26,7 @@ Use-Case**, nicht auf „Drop-in-Replacement für Keycloak". - Multi-Instance / HA (DataProtection + Caches + Rate-Limiter alle In-Memory) -- Enterprise-SSO (kein SAML, kein LDAP/AD) +- Enterprise-SSO (SAML 2.0 SP ✅ seit 2026-05-28 / PR #17; LDAP/AD weiter offen) - Fremde Kunden mit Compliance-Audit (kein SOC2/ISO27001) - Mehr als ~10 aktive Realms ohne eigenes Backup-Tooling @@ -55,7 +55,7 @@ Produkte. | 2a | Deployment-Hygiene (DataProtection persistent + Wolverine-Mode-Toggle) | HIGH | [ha-multi-instance](./ha-multi-instance) | ✅ **DONE 2026-05-13** | | 2b | Echte HA / Multi-Instance (Cross-Instance Pub/Sub, Distributed Caches) | HIGH | [ha-multi-instance](./ha-multi-instance) | ⏸ Deferred — braucht echtes Multi-Box-Setup zum Testen | | 3 | Realm-Backup / Restore / DR-Tooling (N Tenant-DBs) | MEDIUM | [realm-backup-restore](./realm-backup-restore) | Captured | -| 4 | Enterprise-SSO: SAML 2.0 + LDAP/AD-Federation | MEDIUM | [enterprise-sso-saml-ldap](./enterprise-sso-saml-ldap) | Captured | +| 4 | Enterprise-SSO: SAML 2.0 + LDAP/AD-Federation | MEDIUM | [enterprise-sso-saml-ldap](./enterprise-sso-saml-ldap) | SAML 2.0 SP ✅ **DONE 2026-05-28** (PR #17 `8fc3df0`) · LDAP/AD weiter Captured | | 5 | Brute-Force Visibility (Login-Alerts + manuelle IP-Blacklist) | MEDIUM | [login-alerts-ip-blacklist](./login-alerts-ip-blacklist) | Captured (2026-05-07) | | 6 | Per-Realm Branding / Theming | LOW | [white-label-customization](./white-label-customization) | ✅ **DONE Phase 1 2026-05-13** ([Branding](/plattform/branding), [Asset Library](/plattform/assets), [Pages Beta](/plattform/pages)) | | 7 | HSM / KMS Integration für Signing-Keys | LOW | (offen — siehe Audit-Note unten) | Captured-here | @@ -108,7 +108,7 @@ ein Enterprise-Kunde es fordert. Aufwand: Mannmonate, nicht Tage. | Password + Lockout | 4 | 4 | 5 | 4 | | 2FA Breadth | **5** | 4 | 5 | 5 | | External IdP Federation (OIDC) | 3 | 5 | 5 | 5 | -| SAML / LDAP / AD | **1** | 5 | 5 | 4 | +| SAML / LDAP / AD | **3** (SAML 2.0 SP ✅ seit 2026-05-28, PR #17; LDAP/AD weiter offen) | 5 | 5 | 4 | | RBAC Granularity | 4 | 4 | 4 | 5 | | ABAC / Policy-Engine | 2 (extern) | 3 | 4 | 4 | | Multi-Tenancy-Modell | 4 (DB-per-Realm) | 4 | 5 | 5 | diff --git a/dev-docs/future-features/saml-federation.md b/dev-docs/future-features/saml-federation.md index f82197b0..99828566 100644 --- a/dev-docs/future-features/saml-federation.md +++ b/dev-docs/future-features/saml-federation.md @@ -4,7 +4,7 @@ title: SAML federation — implementation plan # SAML federation — implementation plan -> **Status:** Plan captured 2026-05-27. Not started. Chosen as the post-v0.5.0 feature wave. +> **Status:** Shipped (PR #17, commit 8fc3df0). SAML 2.0 SP is live in develop — SamlLoginFlow, DynamicSamlSchemeManager, fully-wired SamlEndpoints (metadata/login/ACS), flavor presets, cert service, metadata refresh, frontend SAML tabs. Single-Logout (SLO) and SAML IdP-mode remain explicitly out of v1 scope (deferred — see [Out](#out-this-wave) / [open questions](#open-questions)). Plan captured 2026-05-27; chosen as the post-v0.5.0 feature wave. > **Why:** Enterprise customers (especially anyone with ADFS or pre-cloud Salesforce/ServiceNow) need SAML 2.0 to even put Modgud on their evaluation list. Today Modgud federates only over OIDC. SAML is the gating capability for the next class of customer conversations. > **Scope of *this* plan:** SAML SP only — Modgud accepts SAML assertions *from* a customer IdP. SAML IdP mode (Modgud emits SAML to legacy apps) is parked, see [open questions](#open-questions). diff --git a/dev-docs/future-features/userinfo-hybrid-flat-emission.md b/dev-docs/future-features/userinfo-hybrid-flat-emission.md index 181888a8..c22f0ce7 100644 --- a/dev-docs/future-features/userinfo-hybrid-flat-emission.md +++ b/dev-docs/future-features/userinfo-hybrid-flat-emission.md @@ -1,10 +1,13 @@ # UserInfo Hybrid-Emission — integriert in das Hauptmodell -> **Status:** Integriert. Siehe [Permission-Modell](./permission-modell) -> §5 für die finale Architektur. UserInfo emittiert **immer** -> per-Audience-nested Blocks mit `permissions`/`roles`/`groups`, -> Bypass-Tiers vom IdP vor-expandiert. Diese Note bleibt nur als -> Designgeschichte bestehen. +> **Status:** Die hier skizzierte **flache Single-Audience-Emission +> wurde verworfen**, nicht gebaut. Geshipped ist die **nested +> per-Audience**-Variante: UserInfo emittiert +> `resource_access[]`-Blocks mit `permissions`/`roles`, +> per-Scope gegated, Bypass-Tiers vom IdP vor-expandiert. `groups` +> wird **nicht** emittiert (IdP-internal). Siehe +> [Permission-Modell](./permission-modell) §5 für die finale +> Architektur. Diese Note bleibt nur als Designgeschichte bestehen. ## Ursprüngliche Idee (überholt) @@ -15,8 +18,9 @@ emittieren könnte für Lib-less-Konsumenten — als opt-in-Optimierung. ## Was am Ende gilt -UserInfo trägt **standardmäßig** Roles + Permissions + Groups, nicht -nur als optionale Hybrid-Emission. Der Audience-Key macht +UserInfo trägt **standardmäßig** Roles + Permissions (Groups bleiben +IdP-internal und werden nicht emittiert), nicht nur als optionale +Hybrid-Emission. Der Audience-Key macht RS-Filterung sauber, die Bypass-Pre-Expansion macht Lib-less-Konsumenten zur First-Class-Option. Die Annahme dass „Distribution-API der einzige Authz-Kanal" sein müsse hat sich nicht diff --git a/dev-docs/future-features/versioning-publishing-conventions.md b/dev-docs/future-features/versioning-publishing-conventions.md index 8d3dabb1..66a767f0 100644 --- a/dev-docs/future-features/versioning-publishing-conventions.md +++ b/dev-docs/future-features/versioning-publishing-conventions.md @@ -4,7 +4,7 @@ title: Versioning & Publishing Conventions # Versioning & Publishing Conventions -> **Status:** Designed 2026-05-28 (split out of [[pr-image-build-on-comment]] when the `/build-image` comment-trigger was deferred but the tag/version rules became a must). Reviewed + revised 2026-05-28. Not yet implemented. +> **Status:** Designed 2026-05-28 (split out of [[pr-image-build-on-comment]] when the `/build-image` comment-trigger was deferred but the tag/version rules became a must). Reviewed + revised 2026-05-28. **Shipped** — GHCR retention (`cd-ghcr-retention.yml`, weekly cron + real prune), moving Docker tags (`cd-publish-staging-image.yml`) and the NuGet prerelease feed-gate to develop (`cd-publish-nuget-prerelease.yml`) are all live workflows. > **Goal:** One version string across the artifact surfaces that *can* carry it (Docker tag, NuGet PackageVersion, AssemblyInformationalVersion), so "which image goes with which package?" is trivial — and clear rules for *where* each build is published, given that some surfaces are permanent and some are cleanable, and that anonymous pull is only possible from some. ## The single version string diff --git a/src/dotnet/Modgud.Api/Features/Auth/OAuth/AuthorizationEndpoints.cs b/src/dotnet/Modgud.Api/Features/Auth/OAuth/AuthorizationEndpoints.cs index 7bb10210..039874d7 100644 --- a/src/dotnet/Modgud.Api/Features/Auth/OAuth/AuthorizationEndpoints.cs +++ b/src/dotnet/Modgud.Api/Features/Auth/OAuth/AuthorizationEndpoints.cs @@ -24,11 +24,10 @@ namespace Modgud.Api.Features.Auth.OAuth; /// /// Minimal-API endpoints for the OpenIddict server: authorize, token, userinfo, -/// logout. This is the porting baseline for stage 3b — it carries enough to -/// satisfy the OpenIddict pipeline (discovery + JWKS work, sign-in/sign-out flow -/// compiles end-to-end), but role + custom-claim injection is intentionally -/// deferred (legacy IRoleRepository / IEffectiveRolesService don't -/// exist in the new authorization model — that bridging is a follow-up phase). +/// logout. Role + permission and custom-claim injection are fully wired: +/// builds the principal and per-audience +/// resource_access block via , then stamps +/// claim destinations with SetDestinations. /// public static class AuthorizationEndpoints { diff --git a/src/dotnet/Modgud.Api/Program.cs b/src/dotnet/Modgud.Api/Program.cs index 897a7d52..0622ce5b 100644 --- a/src/dotnet/Modgud.Api/Program.cs +++ b/src/dotnet/Modgud.Api/Program.cs @@ -624,8 +624,8 @@ builder.Services.AddScoped(); builder.Services.AddHostedService(); - // SAML SP federation (placeholder hook — incremental wiring lands - // in subsequent commits on feat/saml-federation). + // SAML SP federation (cert services, login flow, scheme manager, + // bootstrap + metadata-refresh hosted services). builder.Services.AddModgudSaml(); // SETUP-01 / Setup endpoint surface eliminated in C15d. First-admin diff --git a/src/dotnet/Modgud.Authentication/Api/ExternalAuth/Saml/SamlEndpoints.cs b/src/dotnet/Modgud.Authentication/Api/ExternalAuth/Saml/SamlEndpoints.cs index 36849627..2fa40a48 100644 --- a/src/dotnet/Modgud.Authentication/Api/ExternalAuth/Saml/SamlEndpoints.cs +++ b/src/dotnet/Modgud.Authentication/Api/ExternalAuth/Saml/SamlEndpoints.cs @@ -26,9 +26,8 @@ namespace Modgud.Authentication.Api.ExternalAuth.Saml; /// /// /// Single-Logout (SLO) is explicitly out of scope for v1 — see -/// dev-docs/future-features/saml-federation.md. The handlers below are -/// stubs returning 501 NotImplemented; the actual SAML protocol flow lands in -/// the ACS-integration commit (task #14 on feat/saml-federation). +/// dev-docs/future-features/saml-federation.md. The handlers below +/// (SpMetadata / Login / Acs) delegate to the live SamlLoginFlow. /// /// public static class SamlEndpoints diff --git a/src/dotnet/Modgud.Authentication/Setup/SamlSetup.cs b/src/dotnet/Modgud.Authentication/Setup/SamlSetup.cs index 5801da0c..5e62cfa2 100644 --- a/src/dotnet/Modgud.Authentication/Setup/SamlSetup.cs +++ b/src/dotnet/Modgud.Authentication/Setup/SamlSetup.cs @@ -72,10 +72,6 @@ public static IServiceCollection AddModgudSaml(this IServiceCollection services) // IdPs advertise the new key from. services.AddHostedService(); - // Still to come in subsequent commits on feat/saml-federation: - // - SP cert generation / rotation services (task #13) - // - Real ACS / login / metadata endpoint logic (task #14) - // - Metadata refresh hosted service (task #15) return services; } }