chore(deps): bump docker/login-action from 3 to 4#14
Conversation
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](docker/login-action@v3...v4) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
LabelsThe following labels could not be found: Please fix the above issues or remove invalid values from |
|
Closing — bundled into the next local commit. |
|
OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting If you change your mind, just re-open this PR and I'll resolve any conflicts on it. |
) * docs(saml): consolidate 6 Q&A decisions + add multi-IdP UX plan-page SAML plan-page: - SLO deferred to v2 (saves 2-3 days in v1, additive later) - IdP-mode on concrete customer demand (lib covers both modes) - Metadata refresh 24h default + per-provider 1h/6h/24h/7d override - Group-claim mapping = JsEval Membership Scripts + Quick-Map UI (Identity-Hub pattern, no pass-through to downstream tokens — consistent with existing OIDC behaviour, see new memory anchor project_identity_hub_vs_federation_proxy_open for the broader product-positioning question that stays open) - Multi-IdP login UX split out into its own plan-page (provider-protocol-agnostic, applies to OIDC + SAML + future protocols equally — not a SAML problem) - Endpoints table updated (no SLO endpoint in v1) - Effort re-estimated: ~11 days (was ~13 with SLO) LoginProviderType.cs: - Saml = 2 doc-comment rephrased: "External SAML 2.0 Identity Provider (Modgud acts as Service Provider)" — was misleadingly worded as if Modgud itself were the IdP - Stale "Phase 2+" markers stripped from Ldap + Kerberos comments multi-idp-login-ux.md (new): - Designspace for Picker vs Email-Routing vs Hybrid - Provider-protocol-agnostic, sits as own future-features wave - Recommendation: Pattern C (Hybrid), can phase as Pattern B → C * feat(saml): add ITfoxtec.Identity.Saml2 + DI placeholder hook Adds ITfoxtec.Identity.Saml2 4.18.0 + .MvcCore 4.18.0 NuGet refs to Directory.Packages.props (BSD-3-Clause, compatible with our Apache-2.0 — see plan-page library audit) and references them from Modgud.Authentication. `SamlSetup.AddModgudSaml()` is a placeholder extension method on IServiceCollection so Program.cs has the wiring slot in place; subsequent commits on this branch fill in the actual registrations (flavor registry, dynamic scheme manager, SP cert services, metadata refresh hosted service) without touching Program.cs again. Build green, 0 errors. * ui(modal): add header-actions slot to ModalLayout The Provider-Edit modal in the SAML wave needs a Flavor picker in the modal header (next to the title) so the body's connection fields can morph based on which flavor is selected. Slot is per- instance template-scoped, so nested modals each get their own independent slot content — no DOM-id conflicts like a Teleport portal would have. MainLayout has a different pattern (hardcoded AppContextSelector + an unused #header-outlet-right portal). Aligning MainLayout to this slot-based pattern is a separate future-feature item; not in scope here. * feat(saml): define SamlFlavorData shape + JSON contract Typed view over the JSON blob stored on LoginProvider.FlavorData when Type == Saml. Shape mirrors dev-docs/future-features/ saml-federation.md FlavorData proposal: - IdP federation metadata (URL or pasted XML) - IdP Entity ID - Denormalised IdP signing certs (cache from metadata refresh) - NameID format URI (default: SAML 2.0 emailAddress) - Security toggles (signed/encrypted assertions, signed response, signed AuthnRequest) — secure defaults: all on except encryption - AttributeMap: logical-claim → SAML attribute URIs (multi-value to absorb cross-IdP naming differences) - AmrMapping: AuthnContextClassRef URI → AMR values for federated- MFA detection - MetadataRefreshIntervalSeconds (default 24h) FromJson() is forward-compatible: tolerates unknown fields, falls back to defaults on missing fields, never throws on shape variance. SamlNameIdFormats holds the on-wire URN constants. 18 unit tests cover defaults, parse paths, round-trip, and the URN-vs-constant pinning. All green. * feat(saml): flavor registry — Generic + EntraID + ADFS Mirrors the OIDC LoginProviderFlavorRegistry shape — parallel ISamlFlavor interface + SamlFlavorRegistry, three concrete implementations. Two registries (one per protocol) instead of one unified to preserve per-protocol type-shape: OIDC needs OidcEndpoints from DeriveEndpoints, SAML needs SamlFlavorData from ApplyDefaults. Forcing them into a single ILoginProviderFlavor would require object-typed returns at the consumption sites. Flavor identity: - GenericSaml — vendor-neutral, no presets, MetadataUrl/Xml only - EntraIdSaml — Microsoft Entra Enterprise App; pre-seeds the http://schemas.xmlsoap.org/.../emailaddress family + groups + Microsoft AMR mapping (MultipleAuthn → mfa) - AdfsSaml — on-prem AD FS; pre-seeds AD claim URIs + windows- authentication AMR; MetadataUrl not required (XML paste path for firewalled deployments) Domain LoginProviderFlavor catalog extended with the three SAML keys. OIDC EntraId vs SAML EntraIdSaml are intentionally distinct — silent collision would be a runtime-auth incident. DI wired through SamlSetup.AddModgudSaml() (placeholder filled in). 22 new flavor + registry tests; 40 SAML tests total green. * dev(saml-idp): local SAML test IdP via simplesamlphp container Adds dev/saml-idp/ as a self-contained docker-compose for spinning up a local SAML 2.0 IdP — kristophjunge/test-saml-idp wrapping simpleSAMLphp with two pre-baked test users. Lets the SAML federation slice be iterated offline without ngrok, EntraID config friction, or HTTPS-cert plumbing. EntraID-specific quirks (claim URIs, NameID quirks, MFA AuthnContext) still need a real EntraID hop with the right tunnel, but the SP code-path itself gets shaken out here first. Endpoints (host): IdP UI http://localhost:8080/simplesaml/ IdP metadata http://localhost:8080/simplesaml/saml2/idp/metadata.php Test users: user1 / user1pass user2 / user2pass SP env vars (ENTITY_ID + ACS + SLO) are placeholders; README documents the steps to swap in the real LoginProvider GUID once a SAML provider has been created in Modgud and recreate the container. * feat(saml): DynamicSamlSchemeManager + per-realm endpoint wiring Mirrors the OIDC DynamicOidcSchemeManager + OidcSchemeBootstrap + event-handler chain for the SAML side. Key architectural difference: ITfoxtec.Identity.Saml2 is NOT an ASP.NET Core AuthenticationHandler like OpenIdConnectHandler, so there's no AuthenticationScheme to register dynamically. Instead, DynamicSamlSchemeManager owns a ConcurrentDictionary<Guid, RegisteredSamlProvider> cache that the SAML endpoint handlers (login / acs / metadata) consume by provider GUID lookup. Components: - RegisteredSamlProvider — lightweight cache value type carrying provider id, realm slug, flavor key, resolved SamlFlavorData - DynamicSamlSchemeManager — Register/Unregister/TryGet/Get* lifecycle, flavor-aware (calls ISamlFlavor.ApplyDefaults), type- discriminator gate (Saml only), realm-context required on register - SamlSchemeBootstrap — IHostedService, walks active realms with TenantContext.Enter, seeds the cache with enabled SAML providers. WOLV-02-style fix: re-enter tenant per realm so the per-tenant Marten session reads the right DB - SamlLoginProviderEventHandlers — six Wolverine handlers parallel to the OIDC ones, route through SamlLoginProviderReRegister which short-circuits non-Saml types - SamlEndpoints.MapSamlEndpoints — three route handlers (sp-metadata GET, /{providerId}/login POST, /{providerId}/acs POST) — login + acs return 404 for unknown providers (defensive — don't disclose existence), 501 NotImplemented for the placeholder bodies until task #14 fills in the actual SAML protocol logic DI: AddModgudSaml() now registers manager + bootstrap; Program.cs wires MapSamlEndpoints(). Verified locally: - 53 SAML unit tests green (40 existing + 13 new for the manager) - 997 / 997 unit tests passing overall — no regressions - Backend boots cleanly, log shows "SamlSchemeBootstrap registered 0 SAML providers across 1 realm(s)" — bootstrap walks realms - GET /saml/sp-metadata → 501, POST /saml/{guid}/login → 404, POST /saml/{guid}/acs → 404 (expected for the stubs / unregistered provider IDs) * feat(saml): per-realm SP signing cert with rotation overlap Tenant-scoped Marten singleton document SamlSpCertificateDocument holds the active + previous SP certificate (PFX bytes encrypted via ASP.NET DataProtection — separate Purpose string from the OIDC client-secret store, so a leak in one domain doesn't compromise the other). Plaintext metadata (thumbprint, NotBefore, NotAfter, created-at, rotation-retire-at) lives alongside so admins / logs can see cert state without decrypting. SamlSpCertificateService (scoped, consumes IDocumentSession) owns the lifecycle: - GetActiveAsync — lazy-generates on first call, returns X509 with private key for signing - GetMetadataCertsAsync — returns the active + (if still in overlap window) previous cert; this is what SP metadata XML advertises - RotateAsync — generates fresh cert, demotes active → previous with 30-day retire timer, installs new active - RetireExpiredPreviousAsync — clears the previous slot once the retire timer has passed; no-op when not due Self-signed cert defaults: RSA 2048, SHA256, 2-year validity, subject CN=modgud-sp-{realm-slug}, SAN dnsName matching realm slug. KU=DigitalSignature+KeyEncipherment, EKU=serverAuth+ clientAuth. DI: SamlSpCertificateStore singleton, SamlSpCertificateService scoped — registered in SamlSetup. Marten schema registration for the doc lands in MartenStoreOptionsExtensions alongside the RealmSettings singleton, same pattern. 13 new unit tests (Store round-trip, Document shape invariants, service-constant stability). Full lifecycle (rotate/retire) gets integration coverage once #14 makes the cert service routable. Backend boots cleanly with the new schema registration; 66 / 66 SAML unit tests green; SAML endpoints still 501/404 (real protocol logic lands in #14). * feat(saml): full ACS callback + login + SP metadata via ITfoxtec End-to-end SAML 2.0 SP protocol implementation against the ITfoxtec library. Three protocol operations now functional, replacing the 501-stubs from the per-realm endpoint wiring commit: - SamlMetadataFetcher — fetches IdP federation metadata (URL or pasted XML), parses EntityID + signing certs + SSO URLs via XDocument. Named HttpClient via IHttpClientFactory so the fetcher stays singleton (matches singleton DynamicSamlSchemeManager). 30- line LINQ-to-XML reader instead of ITfoxtec's full metadata model — we use 4 fields, not 40. - DynamicSamlSchemeManager — RegisterAsync now triggers metadata fetch on URL or parses pasted XML, populates RegisteredSamlProvider with the parsed SamlIdpMetadata so endpoint handlers don't re-fetch. Manager flips to async (was Task.FromResult before). - SamlContextBuilder (scoped) — per-request Saml2Configuration factory. Loads SP cert via SamlSpCertificateService, builds the IdP signing-cert list from cached metadata, derives SP EntityID + ACS URL from the live HttpContext. - SamlLoginFlow (scoped) — orchestrates the three operations: * StartLoginAsync: build AuthnRequest (signed per FlavorData toggle), redirect via HTTP-Redirect binding; returnUrl rides RelayState round-trip. * HandleAcsAsync: parse SAMLResponse via Saml2PostBinding, validate against the IdP's signing certs, extract NameID + attributes, build OIDC-shaped ClaimsPrincipal (iss=IdP EntityID, sub=NameID, plus the attribute-map-translated logical-name claims), hand off to ExternalLoginProcessor, sign in via SignInManager, redirect to RelayState returnUrl (relative-paths only, open-redirect-safe). * BuildSpMetadataAsync: emits SP metadata XML with our active + overlap-window-previous certs, ACS URL, NameIDFormat preference. - ExternalLoginProcessor — type-discriminator gate widened from Oidc- only to Oidc OR Saml. Rest of the processor (rawClaims dict, user- update script, JIT, link-or-create) is protocol-agnostic and unchanged. - CreateLoginProviderCommand — SAML branch added. Validates the Flavor against SamlFlavorRegistry, seeds FlavorData with the flavor's defaults (EntraID Microsoft URIs, ADFS AD URIs, etc.), creates the LoginProvider record with Enabled=false (admin enables after smoke-test). Replaces the old `Create_SamlType_NotYetSupported` hard gate. - SamlEndpoints — wires the three routes to SamlLoginFlow. SP metadata path becomes per-provider (/saml/{guid}/sp-metadata) since each provider has its own ACS URL. Integration test `Create_SamlType_NotYetSupported` rewritten as two new tests: `Create_SamlType_With_KnownFlavor_Succeeds` (happy path) and `Create_SamlType_With_UnknownFlavor_Rejected` (validation). Wolverine source-gen handler signature regenerated for the new ctor (SamlFlavorRegistry added alongside the OIDC one in CreateLoginProviderHandler). Verified locally: builds clean, backend boots, all 3 SAML endpoints route correctly (404 for unknown provider IDs — defensive, no silent enumeration). * feat(saml): periodic IdP metadata refresh background service SamlMetadataRefreshService (BackgroundService) wakes every 15 min and re-fetches metadata for any cached provider whose per-provider cadence (FlavorData.MetadataRefreshIntervalSeconds, default 24h, override range 1h/6h/24h/7d) has elapsed since last fetch. Picks up IdP cert rotations ahead of the activation date — most IdPs advertise the next signing key in metadata 1-2 weeks before flipping. DynamicSamlSchemeManager: - Constructor gains TimeProvider so cache entries can be timestamped - New RefreshMetadataAsync(Guid) refetches IdP metadata for a single cached provider and updates the entry in place; returns false (and keeps stale cache) on fetch failure - Cert-change detection logs a clear info line on rotation transitions so admins / log search can see when an IdP rotated keys RegisteredSamlProvider grew a MetadataFetchedAt timestamp slot, populated on register + refresh, consulted by the background service to decide which entries are due. Failures keep the stale data — the cache never goes empty just because a single refresh failed. Bootstrap remains the only path that can leave a provider without any metadata at all. DI: hosted service registered in SamlSetup.AddModgudSaml alongside the cold-start bootstrap. Verified: backend boots clean with the refresh service ticking; 66 / 66 SAML unit tests green; manager test wiring updated for the new TimeProvider ctor arg. * feat(saml): admin UI exposes SAML flavors + types in picker Phase 1 of the login-providers UI work landed alongside the SAML wave — SAML providers are creatable, listable, and editable via the existing two-step add-flow + detail-modal pattern. Single-modal refactor + Quick-Map UI for groups is deferred as a separate wave, captured at dev-docs/future-features/login-providers-ui-refactor.md. Changes here: - Backend FlavorDto.Type — new field ("Oidc" / "Saml") so the admin UI knows which protocol family each flavor implements - /api/admin/login-providers/flavors endpoint concatenates the OIDC registry's flavors with the SAML registry's flavors, both tagged with their Type. Unified list keeps the picker simple - Frontend FlavorDto + store consumes the Type field; LoginProvider- List.vue infers the LoginProviderType to send on Create from the picked flavor (OIDC flavors → Type=Oidc, SAML flavors → Type=Saml) - placeholderFlavorData() — SAML branch returns null since SAML config is supplied on the detail modal (MetadataUrl / Xml are filled there), not at create time - FlavorConnectionPanel.vue — adds MultilineText input variant (a styled textarea) so SAML's MetadataXml field renders with usable height instead of a single-line input Build green; ts type-check green; backend bootable; UI exposes GenericSaml / EntraIdSaml / AdfsSaml in the Add Provider picker alongside the existing OIDC flavors. * feat(saml): end-to-end smoke against simplesamlphp + docs Wired up the final fixes needed for the SAML flow to complete a real SP-initiated login against the dev simplesamlphp IdP: - UpdateLoginProviderCommand — branch on Type (Oidc vs Saml) and validate the flavor against the right registry. OIDC keeps its DeriveEndpoints validation; SAML defers metadata validation to the manager's fetch path - LoginProviderLifecycleCommands.EnableHandler — type-aware preflight: Oidc requires ClientId + Secret, Saml requires MetadataUrl or MetadataXml in FlavorData. Internal stays unchecked - SamlFlavorData.FromJson — case-insensitive property lookup that prefers the variant carrying an actual value, so the round-trip works when the document carries both lowerCamel (canonical serialisation) and PascalCase (frontend FlavorConfigField keys) - SamlLoginProviderReRegister — pull the tenant slug from the session.TenantId when no ambient TenantContext is set (Wolverine event-handler context), enter the scope before calling manager.RegisterAsync. Without this the runtime register-on-event path threw because Wolverine handlers run outside RealmMiddleware - dev/saml-idp — env vars updated to reference the real provider GUID end-to-end (SP EntityID + ACS URL both consistent with what Modgud's SP metadata + AuthnRequest emit) Verified end-to-end against simplesamlphp: 1. SP-initiated login at /saml/{guid}/login generates a signed AuthnRequest, redirects to IdP via HTTP-Redirect binding 2. IdP authenticates user1 / user1pass 3. IdP form-POSTs the SAML Response to /saml/{guid}/acs 4. ACS validates signature, audience, claims; hands off to ExternalLoginProcessor; SignInManager signs in 5. Backend log: "External identity linked to existing user ... via IdP ..." 6. Redirect to RelayState returnUrl issued (302 OK) docs/admin/saml-federation.md — admin-facing guide covering the add-provider flow, IdP wiring, SP metadata URL, cert rotation, troubleshooting, scope notes (SLO + IdP-mode out of scope for v1). * feat(saml): admin Allgemein tab shows SP-metadata + ACS URLs for SAML providers The OIDC-shaped Redirect-URI field was meaningless for SAML providers — admins would have copied /signin-oidc/<guid> into EntraID and broken the setup. SAML providers now expose the two URLs the IdP actually needs (SP metadata for the "App Federation Metadata URL" field, ACS for the "Reply URL" field), both read-only with copy buttons. OIDC providers keep the single Redirect-URI field unchanged. Backend: LoginProviderDto gains SamlSpMetadataUrl + SamlAcsUrl, populated only when Type=Saml using the SamlEndpoints path templates ({providerId:D} canonical form, matching SamlContextBuilder). Frontend: Allgemein tab branches on provider.Type === 'Saml' to render the SAML two-URL block instead of the OIDC redirect URI. Verified against the SimpleSAML-Test provider; OIDC provider regression-clear. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(saml): retarget unsupported-type processor test to Ldap The test asserted that non-OIDC providers surface LoginProvider.TypeNotSupported, but used Type=Saml — which now flows through ExternalLoginProcessor since SAML is a supported protocol. Ldap (and Kerberos) remain the unsupported enum values the runtime gate at ExternalLoginProcessor.cs:56 still rejects. The event-stream setup (StartStream<LoginProvider>) bypasses the create-command validation that also rejects Ldap/Kerberos, so the test still exercises a real defensive code path: a stale Ldap provider somehow present in the stream gets refused at runtime instead of producing a missing-flavor NPE. 163/163 integration tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(login-providers): CreateLoginProviderCommand accepts full provider state Today's two-step Add flow (Create with minimal payload → reopen as Edit modal → Update with the rest) is an outlier vs Auth0 / Keycloak / Okta. The first step toward the single-modal refactor is teaching the Create command to accept everything in one go. CreateLoginProviderCommand gains the same optional fields UpdateLoginProviderCommand already carries (Enabled, ClientId, Scopes, UserUpdateScript, StoreRawClaims, RawClaimsRetentionDays, AutoCreateUsers, AllowLinking, TrustForEmailLink, AllowedEmailDomains, IconName, ButtonColorHex). Each is nullable; null falls back to the flavor's default — preserving the existing two-step caller's behavior bit-for-bit. ScriptInputLimits guard runs on UserUpdateScript at Create too when provided, matching the Update path. Secret rotation stays on its own command for audit clarity. Internal-typed creates ignore the extended fields (the seed is fixed). SAML and OIDC paths both honor them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(login-providers): unified Add+Edit modal with flavor picker in header Eliminates the two-step "open inline picker → submit minimal Create → re-open as Edit modal → fill in everything else" pattern. The login-provider modal now hosts both flows. Header-actions slot carries a flavor dropdown (grouped OIDC · / SAML ·). In Add mode (id='create') it's active and switching morphs the body — Connection tab gates ClientId/Scopes/Secret behind isSaml, FlavorData clears between flavors so each ConfigSchema starts from scratch, and the per-flavor defaults (Scopes, UserUpdateScript, StoreRawClaims, IconName) re-seed automatically while admin-typed identity stays intact. In Edit mode the picker locks (Type/Flavor are immutable post-create) and just labels what the provider runs on. Save in Add mode posts the full provider state in one Create call (using the extended CreateLoginProviderCommand) and — for OIDC — fires RotateClientSecret right after if the admin entered an initial secret. The modal then re-routes its fragment to the new id, transitioning to Edit mode in-place so the SP/ACS URLs become visible without a re-open. Enabled defaults to false at Create — admin opts in explicitly via the toggle that appears in Edit mode. LoginProviderList drops the inline overlay (and the placeholderFlavorData hack that papered over OIDC's Required ConfigSchema at minimal-Create time) and just calls navigateToModal('create'). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(login-providers): mark Phase 2a (single-modal) shipped, Quick-Map deferred Phase 2a — single-modal Add+Edit with header-actions flavor picker and the backend Create-with-full-state extension — landed this wave. Phase 2b (Quick-Map UI for groups) stays deferred and the page now spells out the hidden backend cost so the next agent doesn't re-discover it: today's MembershipScript is a per-Group boolean predicate over a projected Principal and never sees raw IdP claims; the Quick-Map generator's claim-driven shape needs a new claims-aware mapping path on LoginProvider before the frontend generator becomes anything more than UI theater. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(saml,login-providers): block 1 — signature enforcement, Create readiness gates, Add-flow required-field validation Three findings from the code-review pass on this branch: 1. SAML signature enforcement (CheckRequiredSignatures in SamlLoginFlow). ITfoxtec's Saml2Configuration carries no WantAssertionsSigned / WantResponseSigned properties — the library validates whatever IS signed but never requires signatures to be present. An IdP could send an unsigned Response / Assertion and ReadSamlResponse would happily return. Post-parse XML check now rejects when admin-configured FlavorData asks for signatures and they aren't there, including per-Assertion signatures (the XML-signature-wrapping defense — a Response-level signature alone doesn't satisfy WantAssertionsSigned). 2. Create readiness gates parity with EnableLoginProviderHandler. An OIDC provider cannot authenticate without a ClientSecret, and Create never carries one (separate RotateSecret command for audit clarity), so Create with Enabled=true is structurally unsafe — refused. SAML providers need IdP metadata to be registered usefully, so Create with Enabled=true requires MetadataUrl or MetadataXml in FlavorData. The single-modal Add UI already hardcodes Enabled=false; these gates catch stale/scripted callers that bypass the UI. 3. Frontend Add-flow regression. The old two-step List dialog auto-seeded placeholder GUIDs into Required ConfigSchema fields so Create couldn't fail validation; the single-modal Add path dropped that and would relay a cryptic backend FlavorDataInvalid back to the admin. createProvider now validates Required fields client-side and auto-switches to the Verbindung tab pointing at the missing fields by Label. 20/20 LoginProvider integration tests + 66/66 SAML unit tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(saml): block 2 — decryption-cert overlap, reject empty-cert metadata, document link-flow gap Two real fixes plus an explicit known-limitation marker for the third item. DecryptionCertificates (plural) — SamlContextBuilder.BuildAsync now uses ITfoxtec's plural decryption-cert surface, populated with [active, previous] from a new SamlSpCertificateService.GetDecryptionCertsAsync method, so an IdP that hasn't refreshed its metadata yet during the 30-day rotation overlap can still encrypt assertions to the OLD SP public key and we can still decrypt them. The singular DecryptionCertificate property was already marked obsolete by ITfoxtec 4.18 — the build-warning goes away as a bonus. SamlMetadataFetcher.Parse — explicitly refuses IdP metadata that has no usable signing certs (KeyDescriptor entries with empty or whitespace-only X509Certificate values, or encryption-only KeyDescriptors). Returns null with a structured log warning so the scheme manager treats the provider as unregistered, instead of caching a structurally-broken entry whose SignatureValidationCertificates list is empty. SameSite=Lax link-flow degrade — KNOWN LIMITATION marker added inline at HandleAcsAsync where the existing-app-cookie lookup happens, plus a dev-docs/future-features/saml-link-flow-samesite.md page that captures the three possible fixes (signed RelayState, SameSite=None+Secure marker cookie, server-side state store) and explains why the fix is blocked on the test server + real-IdP verification that the simplesamlphp dev rig can't reproduce (it's on localhost, the bug is cross-site only). 66/66 SAML unit tests + 4/4 SAML integration tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(saml,login-providers): block 3 — cert disposal, rotation cooldown, stale-cache eviction, Type-aware Update Five hygiene + correctness fixes from the code-review pass. Saml2RequestContext (cert disposal). SamlContextBuilder.BuildAsync now returns a disposable wrapper that owns every X509Certificate2 it constructs (active + previous SP certs from GetDecryptionCertsAsync, every IdP signing cert parsed from metadata). Saml2Configuration is not IDisposable and ITfoxtec never disposes the certs it borrows, so under sustained SAML traffic the native CNG/CAPI key handles would otherwise leak until GC finalisers caught up. Call-sites in SamlLoginFlow.StartLoginAsync and HandleAcsAsync use `using var ctx = ...` so handles return to the OS immediately on response. SamlLoginFlow.BuildSpMetadataAsync got the same treatment via try/finally — the SP-metadata endpoint is AllowAnonymous and a scraper hammering it was the easiest DoS surface. Switched the IdP-cert constructor from the obsolete X509Certificate2(byte[]) to X509CertificateLoader.LoadCertificate as a free upgrade — the build warning goes with it. Rotation cooldown. SamlSpCertificateService.RotateAsync now refuses a second rotation while the previous-cert slot is still in its 30-day overlap window. The original buggy behaviour silently dropped the cert IdPs were most likely still cached on, breaking AuthnRequest signature validation across the federation for ~24h until each IdP refreshed metadata. Admin can wait for RetireExpiredPreviousAsync to clear the slot, or — if the cert is compromised — needs a future force-rotate escape hatch. Stale-cache eviction on Type change. SamlLoginProviderReRegister was returning early without touching the cache when an Updated event arrived for a non-SAML record. That path is only reachable via direct event-stream surgery or a future force-recreate that recycles the Guid, but if it does happen the manager keeps a stale SAML entry under a now-OIDC id. Now it unregisters on the way out so SAML lookups don't hit the wrong-protocol entry. Type-aware Update. UpdateLoginProviderHandler now forces Scopes=[] + ClientId=string.Empty on SAML providers regardless of what the client submits. Previously a stale UI or scripted caller could store non-empty Scopes/ClientId on a SAML record — a data anomaly that admin grids / exports might mis-classify as OIDC. 168/168 integration tests + 66/66 SAML unit tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(admin): public docs sync with single-modal Add+Edit flow The login-providers + saml-federation pages both walked through the old two-step flow ("click Erstellen → detail modal opens → switch tabs → fill in everything else"). Phase 2a of the login-provider refactor collapsed that into one modal where the admin fills every tab before clicking Erstellen. login-providers.md — Entra walkthrough rewrites the "In Modgud" section to describe filling Verbindung + User-Update-Script tabs BEFORE Create, calls out that the Redirect-URI field only appears post-Create (the provider id is needed to derive it), and adds the optional "Initiales Secret" field. SAML 2.0 added to the supported-types list. saml-federation.md — same shape: pick flavor via header dropdown, fill the Verbindung MetadataUrl tab BEFORE Create, then the SP-Metadata + ACS URLs appear post-Create. Plus a "you can enable on Create" tip since the backend now allows it when readiness conditions are met. Also fixes a pre-existing dead link to ./membership-scripts (the page is groups.md, section Auto-membership) that landed during the SAML wave and was quietly blocking vitepress build. Both VitePress builds (docs/ public + in-app variant) green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(dev-docs): plan PR-image build via /build-image comment Captures the design discussion from the PR #17 review session: per-PR Docker image building gated on an explicit /build-image comment in the PR, with a belt-and-braces required-checks-green verification inside the workflow. Designed for both human reviewers and AI-agent flows (three-line gh CLI sequence: create PR, gh pr checks --watch, gh pr comment with the slash command). Coupled to the still-open GHCR retention policy item — both decisions land together so per-PR image tags don't accumulate unbounded. Path: dev-docs/future-features/pr-image-build-on-comment.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(codeql-triage): extend cs/log-forging FP list with 4 SAML findings PR #17 surfaced four new cs/log-forging alerts via the CodeQL review-comments bot, all on structured-logging calls in the SAML slice (DynamicSamlSchemeManager + SamlSpCertificateService). Pattern is identical to the 10 already-documented FPs — admin-provided values (config.DisplayName, realmSlug, config.Flavor, idpMetadata.EntityId) flow into _logger.LogXxx(...) calls as structured-template properties, not string-concatenated into the message template. Serilog renders them as escaped JSON property values; CRLF in the input cannot forge log entries. Count updated 10 → 14. Dismissals of the new alerts in the GitHub Security UI are pending — once that's done, the open-alert count for this rule on the Code Scanning page should go to zero again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): GitVersion + defensive version-build against empty pre-release-label Workflow_dispatch on feat/saml-federation hit an `Invalid NuGet version string: '0.5.0-.27'` build error. Two-layered bug: GitVersion.yml — the `feature:` regex matched `feature|bugfix|fix` but not `feat`, the actual Cocoar branch-prefix convention (see conventional- commit messages on this repo). Branches like `feat/saml-federation` fell through to the `other:` rule, which in this GitVersion version doesn't materialise the `label: alpha` into the `pre-release-label` output — emitted an empty string instead. Added `feat` to the alternation so the intended `label: '{BranchName}'` template fires. cd-publish-staging-image.yml + cd-publish-nuget-prerelease.yml — both unconditionally interpolated `${major-minor-patch}-${pre-release-label}`, so an empty label produced a trailing `-` that combined with the commit- count `.N` suffix to yield the malformed `0.5.0-.N`. Replaced with an if-empty fallback chain: pre-release-label → escaped-branch-name → drop the segment entirely. Belt-and-braces: even if GitVersion config gains a new gap in the future, the workflow can't emit an unparseable string. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(dev-docs): extend pr-image plan with tag-strategy section The PR-image-build plan already covered when to build (slash-command), which workflow listens (issue_comment), and how to clean up (GHCR retention). Surfaced today on PR #17's manual workflow_dispatch: the existing cd-publish-staging-image.yml tags ONLY `:staging`, regardless of trigger source, so a non-develop dispatch overwrites the develop-published image. Added a tag-strategy section that proposes the develop-vs-non-develop tag matrix (`:staging` stays sacred to develop; non-develop builds get `:<branch-slug>` + `:<version>`), spells out the test-server pull contract, and sketches the workflow change. The /build-image comment-trigger workflow will reuse this matrix once it lands — same conditional, single source of truth. Still deferred. Push went into the existing plan-page rather than spawning a new one because the two concerns share the same workflow and the same GHCR-retention coupling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(login-providers): skip ConfigSchema Required gate for SAML Create The Required-field client validation I added in block 1 of the code-review fix-sweep was too eager for SAML — it iterated flavor.ConfigSchema.filter(f => f.Required) regardless of Type, so SAML's MetadataUrl-required-but-not-at-Create field blocked the Add modal. That contradicts the documented + tested SAML flow: CreateSamlAsync intentionally accepts an empty FlavorData and lands the provider disabled; the admin pastes IdP metadata in the Verbindung tab and hits Speichern afterwards (then Enable). The EnableLoginProviderHandler readiness gate (LoginProviderLifecycleCommands.cs:38-46) catches metadata-missing at Enable time, which is where it belongs. Gate now only applies when flavor.Type === 'Oidc' — OIDC's DeriveEndpoints does throw on empty Required fields at Create, so the client-side guard is still valuable there. Spotted during the EntraID smoke setup on auth.cocoar.dev when the Add modal blocked with "Pflichtfelder fehlen im Verbindung-Tab: Federation Metadata URL" even though the admin's intent was to proceed with empty metadata and fill it after Entra setup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(saml): FlavorData casing tie-breaker prefers PascalCase admin input Smoking-gun bug surfaced during the EntraID smoke setup on auth.cocoar.dev: after the admin pasted a new IdP metadata URL into the Verbindung tab and saved, Modgud kept fetching the previous (placeholder) URL. Two coupled bugs in the FlavorData camelCase-vs-PascalCase reconciliation. Backend (SamlFlavorData.TryGetPropertyCaseInsensitive): when BOTH forms were present with real values, the helper picked camelCase. The camelCase value is whatever ToJson wrote previously, so on Update it represents the PRE-edit state that the frontend spread back into form.FlavorData via existing.FlavorData. The PascalCase value is the admin's freshly typed input. Preferring camelCase silently overwrote the edit with the stale value. Flipped the tie-break: PascalCase wins when non-null; falls back to camelCase when PascalCase is null/undefined (forwards-compat for clients that explicitly clear the field). Frontend (LoginProviderDetails.normalizeFlavorData): on modal reload the spread of existing.FlavorData carried camelCase keys, but FlavorConnectionPanel reads modelValue[field.Key] in PascalCase — every SAML connection field rendered empty even with a stored value, hiding the actual state from the admin. New normaliseFlavorData() promotes any schema-known camelCase key to PascalCase (and drops the camel variant) at load time. Schema-unknown keys stay untouched for forwards-compat. Two new SamlFlavorDataTests cover both tie-break directions (PascalCase-wins-on-both-non-null + camelCase-wins-when-PascalCase-null). 20/20 SamlFlavorData unit tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(login-providers): import FlavorConfigFieldDto in LoginProviderDetails The normalizeFlavorData helper added in 57dd275 references FlavorConfigFieldDto but the type was never added to the import line, breaking the frontend type-check in CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(login-providers): guard pascal[0] under noUncheckedIndexedAccess vue-tsc --build (the type-check script's strict project config) flags pascal[0] as possibly undefined. charAt(0) returns a string for every index, so the camelCase derivation type-checks without a guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(login-providers): admin-chosen immutable slug in provider URLs Replace the aggregate Guid in the public provider URLs with an admin-chosen, per-realm-unique, immutable slug so a delete + recreate keeps the same URLs (no re-pasting into the upstream IdP): OIDC callback /signin-oidc/{guid} → /signin-oidc/{slug} SAML SP surface /saml/{guid}/{action} → /saml/{slug}/{action} Backend - LoginProvider.Slug + LoginProviderAddedEvent.Slug (immutable: no Update event field); projection + Internal seeder ("internal"). - LoginProviderSlugRules (format only; no reserved list — the route shape can't collide and per-realm uniqueness guards the seeded slug). - CreateLoginProviderCommand: required Slug, format + per-realm-uniqueness validation (SlugInvalid / SlugTaken) ahead of the type-specific paths. - SAML: routes keyed by {slug}; DynamicSamlSchemeManager.TryGetBySlug (realm, slug) lookup (realm from TenantContext); SamlContextBuilder SP-EntityID + ACS URL by slug. - OIDC: callback path by slug. Since slugs are only per-realm-unique and the framework callback match is host-blind, add HostAwareOpenIdConnect- Handler that disambiguates by the request's realm (RealmMiddleware) vs the scheme's realm, tracked in an in-memory OidcSchemeRealmRegistry. Scheme name stays Oidc_{guid} (global uniqueness); the slug never enters a scheme name. - LoginProviderDto.Slug; RedirectUri / SamlSpMetadataUrl / SamlAcsUrl by slug. Frontend - Slug field in the unified Add+Edit modal (above DisplayName), required + format-validated on create, read-only in edit, auto-suggested from the Display Name. Model + de.json. Docs: login-providers.md + saml-federation.md document the slug as a create-time input and the recreate-stability it buys. Tests: LoginProviderSlugRules unit tests; SAML TryGetBySlug + cross-realm same-slug disambiguation; command-level SlugInvalid / SlugTaken. Existing OIDC manager tests enter TenantContext + assert the host-aware handler type. Unit 1035/1035, integration 170/170, frontend type-check green. No migration — pre-release, no existing providers to backfill. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(login-providers): escape {slug} in slug hint so i18n doesn't eat it The create-mode slug hint rendered ".../signin-oidc/)" because the vue-localization interpolator consumed the literal {slug} token. Use <slug> as a non-interpolated placeholder. Surfaced during the local DevTools + simplesamlphp smoke. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(login-providers): Display Name + Slug side by side, live slug derivation - Lay Display Name (left) and Slug (right) out in one row (.lp-name-row); when the Slug field is hidden (Internal provider) Display Name fills it. - Derive the slug from the Display Name LIVE as the admin types (was: only on blur), normalised to the slug grammar (lowercase, non-alphanumerics → hyphens, collapse/trim). Spaces become hyphens, not underscores — the backend LoginProviderSlugRules forbids underscores. - Stop auto-deriving once the admin hand-edits the slug; clearing it re-arms the sync. Tracked via slugManuallyEdited + onSlugInput. Verified in-browser (DevTools): "Acme SSO (Prod)!" → "acme-sso-prod"; manual slug edit survives a subsequent Display Name change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(login-providers): cover HostAwareOpenIdConnectHandler realm tiebreaker Direct tests for ShouldHandleRequestAsync — the per-tenant callback disambiguation that lets two realms share a slug-based /signin-oidc/{slug} path. Covers: realm match → handle; same path, different realm → decline (the core multi-realm guarantee); path mismatch → decline; untracked scheme → fall back to base. No host needed — handler built with a stub options monitor + hand-built HttpContext. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(codeql-triage): add cs/log-forging alert 37 (OIDC scheme registration) PR #17 review surfaced alert 37 on DynamicOidcSchemeManager.cs:234 — the OIDC mirror of the already-dismissed SAML registration log line. Audited on its own merits (structured props: identifier scheme name, admin-only DisplayName, validated flavor key, RealmSlugRules-validated realmSlug), dismissed as false positive in the UI. Count 14 → 15. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(login-providers): show SAML providers as login + profile buttons The /api/account/external-logins discovery endpoint returned OIDC providers only, so enabled SAML providers never rendered a login-page button (and the SP-initiated flow was unreachable from the UI). Include SAML providers and add Kind + Slug to the DTO; the login + profile pages branch the entry-point URL by Kind: OIDC → /api/account/external-login/{id}/start, SAML → /saml/{slug}/login. Backend builds, frontend type-checks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(signalr): scope realtime push to the originating realm The SignalR live-update layer fanned every CRUD DataEvent through one process-global dispatcher to all connections, with the per-connection Subscribe() filtered by subject only — no tenant scoping. A user created in realm A appeared live in realm B's admin grid (verified bidirectionally). Masked in prod only because tenant realms couldn't connect to /signalr at all; the RealmMiddleware connection fix here unmasks it, so both ship together. Fix (single-node-correct, multi-node deliberately deferred): - DataEvent carries an originating Tenant tag; DataEventDispatcher gains tenant-stamping overloads. - Producers stamp session.TenantId (SignalRProjectionDispatchHandler for User+Inbox, ServiceAccountsEndpoints for ServiceAccount). - Each Subscribe() filters by the connection's authoritative realm (HttpContext.Items["TenantId"], resolved at connect by RealmMiddleware). Untagged events match no realm -> dropped (fail-closed, never leaked). - RealmMiddleware: /signalr no longer skips realm resolution, so per-tenant cookies decrypt and tenant realms can connect. Verified in the 2-realm container: cross-realm create does NOT appear in the other realm's grid; same-realm live update still works; both directions. Unit 1035/1035, integration 174/174. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(saml): default WantResponseSigned off + expose signing toggles in admin UI EntraID (and AD FS, and most IdPs) sign only the SAML Assertion, not the Response wrapper, by default. Modgud's SAML providers defaulted to also requiring the Response wrapper signed (WantResponseSigned=true) with no way to change it in the admin UI, so an Entra SP-initiated login failed with "saml-response-unsigned" right after authentication. - WantResponseSigned now defaults to false (record + FromJson). Assertion signing (WantAssertionsSigned=true) stays on — that's the real XML-wrapping defense, so this is not a security regression. - Expose the three SAML signing toggles (WantAssertionsSigned, WantResponseSigned, SignAuthnRequest) as Boolean fields in every SAML flavor's Connection tab via a shared SamlSigningConfigFields, so admins can match the toggles to what their IdP actually signs. - FlavorConfigField / FlavorConfigFieldDto gain a Default; the admin form seeds field defaults on create so checkboxes reflect the real default instead of rendering unchecked while the backend applies a different one. Verified in the 2-realm container: existing provider shows its stored value (Response checked); a fresh Entra SAML provider defaults Response unchecked, Assertions + AuthnRequest checked. Unit 1035/1035 (SAML 20), frontend type-check green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(saml): shared advanced schema, Advanced + Claim-Mapping tabs, live SP URLs SAML is one protocol, so the set of advanced knobs is identical across all flavors — flavors should differ only in default values, not in which settings exist. Make that structural so a flavor can't silently omit a knob (the class of bug that hit Entra with WantResponseSigned). - FlavorConfigField gains Section + a Select field type with Options (+ the Default added earlier); FlavorConfigFieldDto + frontend model mirror it. - SamlSigningConfigFields → SamlAdvancedConfigFields: one shared advanced set (signing toggles + WantAssertionsEncrypted + NameIdFormat select + EntityId + metadata refresh-interval select), Section="advanced", spread into all three SAML flavors. FromJson tolerates a string-encoded refresh interval (Select submits strings). - Frontend: section-aware FlavorConnectionPanel (+ CoarSelect rendering), new "Advanced" tab (SAML) and "Claim-Mapping" tab. Defaults seeded into the form on create so checkboxes/selects show the real default. - ClaimMapEditor: structured key→string[] editor for AttributeMap (claim → IdP attribute URIs) and AmrMapping (AuthnContextClassRef → AMR), so admins can fine-tune when an IdP changes a claim URI instead of being stuck with the seeded defaults. - SAML SP-Metadata + ACS URLs are now derived live from window.location.origin + the slug (create + edit), instead of the backend DTO's configured PublicUrl which could surface an internal/unreachable host (showed 0.0.0.0:8081 in the rig). Matches the runtime SamlContextBuilder (req.Host). Verified in the 2-realm rig: OIDC shows no Advanced/Claim tabs; SAML Entra create shows both tabs, correct advanced defaults (response unsigned, NameID email, refresh 24h), and the live SP URL with the right host before save; AttributeMap row round-trips through save + reopen. Unit 1035/1035, frontend type-check green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(saml): enabled switch + grid-based claim-mapping editor UI polish on the SAML admin surface: - The provider enable/disable control is now a CoarSwitch (was a button), bound to provider.Enabled as the source of truth so a failed enable (e.g. the SAML readiness gate) snaps it back and surfaces the error. - ClaimMapEditor rebuilt on CoarGridBuilder/CoarDataGrid — same inline-edit pattern as EditableStringList (two editable columns + delete icon + add toolbar) instead of a hand-rolled layout, for consistency with the other admin grids. Verified in the 2-realm rig: switch toggles enabled/disabled (grid reflects Nein/Ja) and reverts on failure; claim-mapping tab renders both maps as grids with headers + add toolbar. Frontend type-check green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(login-providers): PATCH update via Optional<T>, fold in enable/disable Replace the full-replace PUT + dedicated enable/disable endpoints with one PATCH-style update where every field is Optional<T> (the same pattern the User/Profile updates already use). Absent fields keep their current value, so: - the edit modal submits the full form (Enabled now a staged switch that commits on Save, not an immediate side-effect), and - the grid's inline toggle submits just { Enabled } — no full provider needed. Enabled folds the former Enable/Disable commands into the update handler: a false→true transition runs the readiness gate (extracted to LoginProviderReadiness, checked against the POST-merge values so "set metadata + enable" works in one save) and emits the distinct LoginProviderEnabledEvent; true→false emits Disabled. A bare Enabled PATCH yields only that audit event, no spurious "updated". The /enable + /disable endpoints and their commands are removed. Verified in the 2-realm rig: partial PATCH changes only Enabled (DisplayName + FlavorData preserved); enable without metadata is rejected (gate); metadata + enable in one PATCH succeeds; the modal switch staged-flip does NOT persist without Save; grid toggle PATCHes immediately. Unit 1035/1035, integration 174/174, frontend type-check green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(login-providers): OIDC advanced settings + live redirect URI Bring the SAML "Advanced tab" treatment to OIDC. OIDC has no rich FlavorData model like SAML, so rather than an empty tab, expose the genuinely useful, handler-consumable OpenIdConnectOptions knobs as real new settings. - OidcAdvancedConfigFields (shared across the OIDC flavors, Section=advanced): UsePkce, GetClaimsFromUserInfoEndpoint, SaveTokens (toggles) + Prompt (select: login / select_account / consent / none). Defaults match what DynamicOidcSchemeManager previously hard-coded, so existing providers are unchanged. - DynamicOidcSchemeManager reads these from config.FlavorData and applies them to OpenIdConnectOptions (Prompt only when set). - Live OIDC redirect URI ({origin}/signin-oidc/{slug}) shown from the slug at create + edit, same slug-derived fix as the SAML SP URLs (replaces the DTO's configured-PublicUrl value that could be an unreachable internal host). The Advanced tab + Boolean/Select rendering already exist (built for SAML), so the OIDC tab appears automatically — no new frontend code; OIDC correctly shows no Claim-Mapping tab (claims map via the user-update script, not a structured map). Verified in the rig: OIDC create shows the Advanced tab with correct defaults (PKCE on, UserInfo on, SaveTokens off, Prompt none); live redirect URI from slug; advanced values round-trip through create + partial FlavorData PATCH. Unit 1035/1035, integration 174/174, frontend type-check green. (A live OIDC auth applying the options wasn't exercised — no OIDC test IdP in the rig; the wiring is a direct options assignment.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(login-providers): enter TenantContext in OIDC scheme re-registration The OIDC event handlers called DynamicOidcSchemeManager.RegisterAsync without an ambient TenantContext, so runtime add/enable/update threw and the scheme was never registered — only the boot-time bootstrap worked. Mirror the SAML handler: pull the tenant from the message-scoped session and enter it before registering. Verified live: add/enable/disable/update now register and unregister the scheme without a restart. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…48) A code-vs-docs audit (15-agent workflow) found the product surface is ahead of what the docs/comments claim: several merged features were still labelled "not started" / "stub" / "deferred". This corrects the framing so future audits don't re-litigate shipped work. Docs + code COMMENTS only — no runtime behavior changes. dev-docs status lines flipped to verified reality (all cited commits are ancestors of develop): - federation-v1-design / -implementation-plan → Shipped (PR #23 4fa3af0, PR #24 0b70b31); Phase 6 per-realm TTL + durable leased-membership v2 kept as the genuine remainder. - saml-federation + index → Shipped (PR #17 8fc3df0); SLO + SAML IdP-mode kept explicitly deferred. - versioning-publishing-conventions → Shipped (GHCR retention, moving Docker tags, NuGet feed-gate are live workflows). - app-resources-as-permissions → ID-anchored model shipped. - white-label-customization (index) → Phase 1 shipped (8c8dea5/2ec0e58/ ae2f9ca); page-builder runtime + custom-CSS kept deferred. - production-readiness-audit → SAML SP DONE (PR #17), rescored 1→3; LDAP/AD kept open. - identity-lifecycle-untangle → auto-membership externalClaims contradiction RESOLVED (PR #24); durable-lease piece kept open. - permission-modell §5 + userinfo-hybrid-flat-emission → corrected to "groups NOT emitted (IdP-internal)" — matches AuthorizationEndpoints.cs + UserInfoPerAudienceTests. (The line a future groups-claim decision would consciously lift; left at today's reality.) False in-code comments removed/corrected (comment-only): - SamlEndpoints.cs: dropped the false "handlers are 501 stubs" note — they delegate to the live SamlLoginFlow. - SamlSetup.cs: dropped the "still to come task #13/#14/#15" block. - Program.cs: SAML hook is wired, not a "placeholder". - AuthorizationEndpoints.cs: claims injection is shipped via IPermissionService, not "deferred / legacy IRoleRepository". - CI workflow comments: :staging → :beta (the tag actually pushed). Deliberately NOT touched: signing-key rotation-overlap docs — those belong to the separate rotation thread (implement-vs-document still open). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ize contract + overflow fix (waves 0–6) (#63) * feat(admin-ui): create-flow defect fixes + German i18n sweep (UI/UX wave 0+1) Wave 0 — three defects surfaced and live-verified by the UI/UX audit: - OAuth client create no longer dead-ends on a bare "HTTP 400 Bad Request": surface the server's actual message (e.g. "client_credentials must be linked to a ServiceAccount ...") in a top-of-modal error banner. Read the response body's `error`/`detail` field instead of the never-populated `Message`. - Roles: the Permissions tab + catalog picker now render in the *create* modal (previously edit-only), so an admin can assign permissions while creating a role. Backend/DTO already accepted PermissionIds on create — this only ungates the three isCreate template guards. - User create/update: reject malformed emails (e.g. "notanemail") client-side (inline error + disabled submit) and server-side (new DomainErrors.User. EmailInvalid + format guard in CreateUser/UpdateUser handlers). Wave 1 — German i18n sweep (de.json was missing ~190 used keys; their English fallbacks leaked into the German UI): - New useGridLocale() composable → German search placeholder ("Suchen…") and empty overlay ("Keine Einträge vorhanden") applied to all 10 list grids. - ~190 missing keys translated (OAuth client/scope/api modals + dual-list labels, roles, groups, apps, realm-settings DCR, profile, consent, login, bootstrap, logout, change-requests, …). - Modal-title verb order fixed ("Erstelle X" → "X erstellen") for user/role/group. - Dropped the "Keycloak-style" competitor reference from user-facing copy. - Reworded "(eine pro Zeile)" labels that actually sit over add-row grids. Verified live against ghcr.io/cocoar-dev/modgud:beta (local cold-start) through the dev server — all five changes confirmed in the browser. FE type-check and BE build both green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(admin-ui): shared grid pass — empty-states, row-open cue, truncation tooltips (UI/UX wave 2) Wave 2 of the admin-UI UX remediation (REPORT.md §7 item 7): one shared change across all 14 admin list/log/queue grids instead of per-view fixes. useGridLocale() is extended into the single shared grid layer: - applyListGridDefaults(builder, { openable }) wires the German locale overlay, a shared defaultColDef, and (opt-in) the row-open affordance. - sharedDefaultColDef carries only tooltipValueGetter (full value on hover for truncated cells). flex/minWidth are deliberately NOT defaulted: in AG-Grid an inherited flex overrides explicit column widths and an inherited minWidth clamps narrow columns up, which would break the fixed/pinned identifier and icon columns. Flex priority is set per-column instead. - Icon columns opt out of the tooltip (() => null) so they never surface the raw lucide name ('check'). - Row-open cue is a pure-CSS pointer + hover class (admin-grid-row--openable), no behaviour change — keeps cell-double-click, selection and context menus. New GridEmptyState.vue renders an onboarding empty-state (icon + one-line concept definition + optional CTA) as a sibling to the grid, gated on the store's readiness flag AND zero *source* rows — so a search/filter-empty grid keeps the grid and its localized "Keine Einträge vorhanden" overlay rather than wrongly telling the user to create the first record. Log/queue views use the no-CTA variant. ServiceAccountsView + AuthLogView + AuditLogView + ChangeRequestsView adopt useGridLocale net-new, which also fixes their untranslated "Search..." / "No Rows To Show" overlays (finding 36). ~14 German emptyHint strings added to de.json with English fallbacks inline. Live-verified against the :beta container: empty-state + CTA→create modal, truncation tooltip, row-hover cue, icon cell shows no raw-token tooltip, German chrome on the net-new adopters. pnpm type-check green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(admin-ui): modal-size contract + form-layout pass — kill the dead half (UI/UX wave 3) Wave 3 of the admin-UI UX remediation (REPORT.md Phase 3) — the owner's core complaint: cramped / unpredictable modals and stretched field arrangement. Modal-size contract (router/index.ts). Replace the per-modal one-off size constants with a named system (MODAL_MD / MODAL_LG / MODAL_FULL) using two height strategies: - cap-to-content (height:auto + maxHeight) for single forms, so the panel sizes to its content instead of forcing 90vh with a dead lower half. Proven by the old SERVICE_ACCOUNT size; drive the family toward ScopeDetails. - stable tall frame (height==minHeight==maxHeight) for tabbed / grid / editor modals whose flex:1 dual-listbox / AG-Grid / Monaco children need a definite ancestor height. Big sizes keep vw width + maxWidth cap and NO minWidth rem floor (the documented viewport-overflow gotcha). Remap: Scope/Realm/Role/ServiceAccount → MD; API → MD with a minHeight floor so selecting an App fills reserved space instead of jumping the frame; User → a cap-to-content fluid size (compact create, no dead half) with the Groups dual-listbox carrying its own explicit height so edit still works; Group keeps a tall frame (Monaco + two dual-listboxes); IdpClaims/LoginProvider/ScheduledJob/ ConsistencyCheck → LG; App/Client → FULL. Form layout: - UserDetails: the email-verified toggle moves OUT of the Email field to the form's end, so it no longer injects between Email and Username (the create layout-jump, #10). General form wrapped in the new max-width column. - main.css: app-level .modal-form-col (~720px form column) + per-field width caps (.field-name/.field-email/.field-enum/.field-num). Controls obey their container, so capping the CoarFormField parent suffices — no vue-ui fork. Applied to UserDetails and the LoginProvider General tab (which must stay wide for its Monaco / dual ClaimMapEditor tabs but should not stretch the form). - New ColorField.vue: hex/CSS-color text input + a swatch that doubles as a live preview and OS color-picker trigger + inline validity (#11). Replaces the raw hex inputs in BrandingView (PrimaryColor) and LoginProvider (Button-Farbe). Live-verified against the :beta container at 1440×900: User create is now cap-to-content (dead half gone) while edit's Groups dual-listbox renders at 50vh; Scope/API/Role compact; Client (FULL) and LoginProvider (LG) stable; confirm-email appears at the form end without shifting fields; ColorField preview updates live. pnpm type-check green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(admin-ui): polish pass — AA primary contrast, header model, maintenance + dashboard a11y (UI/UX wave 4) Wave 4 of the admin-UI UX remediation (REPORT.md Phase 4 polish). Honest corrections to the brief's framing after re-verifying each claim: #12 Primary-button token / login contrast. Login and admin do NOT diverge — the login submit button omits `variant`, so it already renders the SAME default `coar-button--primary` as admin primaries. The real defect is the shared `--coar-accent` (#1183CD) giving only ~4.08:1 white-on-blue, below WCAG AA. One root override in main.css — `--coar-accent: #1077be` — lifts every primary button (login submit, admin variant=primary, the modal footer confirm) to 4.77:1 at once, keeping the hue and staying above the ramp's fixed hover/active lightness so buttons still darken on hover. The realm-create CTA and modal-confirm-as-footer-anchor were already satisfied by the wave-3 ModalLayout. #13 Header model. Converge the two breadcrumb-array outliers onto the app-wide string-subtitle model: ScheduledJobList (its leading "Administration" crumb only duplicated the title); PageEditorView keeps its hierarchy as text ("Pages · <name>"). The dashboard stays title-only (a landing page, deliberately exempt). #15 Maintenance + observability. The destructive "Rebuild Projections" fired immediately — now it sits behind a CoarPopconfirm and uses the danger variant so it no longer reads as a benign action (mirrors RealmSettings' rotate-key twin). The observability sparkline drew a blank chart on an empty window — now shows an empty-state message, like the activity/error feeds already do. #14 Dashboard. Measured a11y/affordance polish, NOT a redesign (tiles already navigate on click): tiles get role=button + tabindex + Enter/Space keyboard activation + a focus ring + a drill-down chevron revealed on hover/focus; the bad/warn KPI values pair their colour with an alert icon + an sr-only "Achtung" so status isn't colour-only; the Login-Provider rows pair the status dot with an "Aktiviert/Deaktiviert" text tag. (The subjective personal-vs-ops reorder was deliberately left out.) Live-verified against the :beta container: primary contrast 4.77:1 with correct hover darkening (resolved oklch ramp checked in-page); ScheduledJobs string subtitle; rebuild danger button + confirm popover; dashboard chevron on hover, role=button, "Achtung" status, provider text tag. pnpm type-check green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(admin-ui): wave 5 — MD form width caps, group-modal cap-to-content, dashboard sections Closes the three deferred follow-ups from the UI/UX remediation (waves 0–4). #1 MD form per-field width caps (Realm/Scope/API/Role/ServiceAccount): tag the full-width single-line fields with the existing .field-* caps — .field-enum (18rem) on the Application selects, .field-name (24rem) on Description / Name / AccountName / Purpose. No new CSS, no .modal-form-col (a no-op at 42rem). Multi-line list editors (RealmDomains, EditableStringList, permission checklist) stay full-width. Also drop two dead ModalLayout width= props (Scope 40rem, ServiceAccount 48rem) — modal size is route-owned and the prop is ignored. #2 Group modal — kill the create "dead half": move GROUP_MODAL_SIZE off the fixed 82vh LG frame to cap-to-content (60vw / 52rem, minHeight 30rem floor) so the create-landing General tab collapses to its content. The heavy edit tabs carry their OWN explicit section height (.editor-section 50vh for Members/Roles/Script, .effective-section 32vh for the read-only Effective lists) so they survive cap-to-content instead of collapsing — .flex-section drops flex:1. Cap the General fields with .modal-form-col + .field-*. Drop the dead width=44rem prop. #3 Dashboard — labelled personal/ops sections: split the single KPI list into personalKpiTiles + opsKpiTiles, rendered under two labelled bands ("Mein Konto" / "Realm-Betrieb"). The ops band collapses entirely for viewers without ops perms. Extract the KPI card into KpiCard.vue (+ kpiTile.ts types) so both grids share it. The ops grid stays count-aware (centered for <=2 tiles) so a lone tile doesn't float alone in a 6-col row. Live-verified @1440x900 against the :beta backend; vue-tsc --build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(admin-ui): wave 6 — universal modal-overflow fix + form-layout redesign Modal overflow (owner blocker): cap-to-content modals (height:auto + max-height) could not internal-scroll once content exceeded the cap. The overlay panel's explicit height:100% resolved to auto against the auto-height host, escaping the max-height clamp — so tall forms laid out at full content height and overflowed the viewport, leaving the footer/Save button unreachable. Dropping height:100% lets the host's flex align-items:stretch size the panel to the *clamped* cross-size: short forms still cap to content (no dead space), tall forms scroll internally with the footer pinned. Definite-height modals (LG/FULL) are unaffected — align-stretch fills the definite height just as height:100% did, so Monaco / dual-listbox / AG-Grid keep a definite ancestor. Form-layout redesign (8 detail modals): one shared grid contract (.modal-form*) replaces content-arbitrary rem-width fields — labelled sections, paired short fields, full-width prose/lists, and visible inline field-hints (vue-ui's :hint is a hover popover only in 2.5.2). Applied to Group, Role, Scope, API, Realm, ServiceAccount, User (General) and LoginProvider (Allgemein); raw checkboxes → CoarCheckbox; German microcopy de-jargoned in de.json. Tab-switch resize fix: tabbed modals keep one fixed size across tabs (Role 33rem, Group 80vh, User-edit pins .user-edit-frame at 60vh). Verified live across all 11 modal types: footer reachable, correct scroll/cap behaviour, no inner-widget collapse. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps docker/login-action from 3 to 4.
Release notes
Sourced from docker/login-action's releases.
... (truncated)
Commits
650006cMerge pull request #960 from docker/dependabot/npm_and_yarn/aws-sdk-dependenc...99df1a3chore: update generated content3ab375fbuild(deps): bump the aws-sdk-dependencies group across 1 directory with 2 up...39d8580Merge pull request #970 from docker/dependabot/npm_and_yarn/docker/actions-to...4eefcd3chore: update generated content56d092cbuild(deps): bump@docker/actions-toolkitfrom 0.86.0 to 0.90.0e2e31caMerge pull request #976 from docker/dependabot/npm_and_yarn/actions/core-3.0.10bced94chore: update generated content3e75a0fbuild(deps): bump@actions/corefrom 3.0.0 to 3.0.1365bebdMerge pull request #984 from docker/dependabot/github_actions/aws-actions/con...Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)