diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index 57bb2e2..a923d6a 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -20,7 +20,7 @@ jobs: env: DAPPER_IMAGE: pasturestack/authentication-service-dapper:${{ github.sha }} TRIVY_IMAGE: aquasec/trivy:0.73.0@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c - VERSION_OVERRIDE: v0.4.40 + VERSION_OVERRIDE: v0.4.41 steps: - name: Check out candidate uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -70,7 +70,7 @@ jobs: } run_ci - artifact="dist/artifacts/authentication-service-0.4.40-linux-amd64.tar.xz" + artifact="dist/artifacts/authentication-service-0.4.41-linux-amd64.tar.xz" test -s "$artifact" cp "$artifact" /tmp/authentication-service-first.tar.xz rm -rf bin dist @@ -81,7 +81,7 @@ jobs: tar -xJf "$artifact" -C evidence/product test -x evidence/product/authentication-service test "$(find evidence/product -maxdepth 1 -type f | wc -l)" -eq 1 - evidence/product/authentication-service --version | grep -F '0.4.40' >/dev/null + evidence/product/authentication-service --version | grep -F '0.4.41' >/dev/null sha256sum "$artifact" > evidence/authentication-service.tar.xz.sha256 docker run --rm --entrypoint go \ --volume "$PWD:/work:ro" \ diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 77f9508..3e55691 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -20,6 +20,9 @@ enabled, unchanged OIDC provider must not repeat discovery, key retrieval, or provider initialization. Initial enablement, changing provider type, or changing the OIDC identity source still requires a fresh local-recovery check and successful provider initialization. +This includes reload requests emitted by platform setting events after the +policy write: an already-live provider adopts the updated access policy in +memory, while startup and source changes still initialize the provider. Expanding access requires a one-time Engine MFA security confirmation bound to the authenticated operator, purpose `oidcAccessPolicyUpdate`, and the canonical diff --git a/README.md b/README.md index 2ca104d..8a0926c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ PastureStack is an independent community effort to preserve, audit, and moderniz ## Project status -The current compatibility release is `v0.4.40`. It retains the existing Ubuntu 26.04, +The current compatibility release is `v0.4.41`. It retains the existing Ubuntu 26.04, Go 1.27.0, JWT, cookie, TLS, LDAP, GitHub, Shibboleth, dependency, and build maintenance. It adds a provider-neutral OpenID Connect authorization-code client with discovery, PKCE S256, nonce validation, @@ -21,7 +21,7 @@ single-use signed identity proof. The control platform uses that proof for an explicit account-link or reassignment decision; profile fields are never trusted as implicit account-matching keys. -Release `v0.4.40` separates OIDC identity-source changes from site-access +Release `v0.4.41` separates OIDC identity-source changes from site-access policy changes. An already-enabled provider can change access mode and its OIDC user/group allowlist without repeating discovery, emitting a provider reload generation, or repeating the five-minute local recovery ceremony. @@ -43,6 +43,11 @@ empty legacy OIDC keys over the authoritative access mode or allowlist. This keeps restricted `oidc_user` and `oidc_group` entries intact across service and Server container restarts while retaining the one-time migration path for old installations. +Platform setting events can request a reload after a policy save. When the +active OIDC provider and identity source are unchanged, that reload now adopts +the persisted access policy in memory without repeating discovery, key +retrieval, or provider construction. Startup, first enablement, provider +switches, and identity-source changes retain the full initialization path. Product-owned imports, executable names, CLI settings, client variables, and operator messages use PastureStack naming. @@ -61,9 +66,9 @@ make build make package ``` -Set `VERSION_OVERRIDE=v0.4.40` for the reviewed identity-security compatibility +Set `VERSION_OVERRIDE=v0.4.41` for the reviewed identity-security compatibility release. Packaging produces the deterministic, versioned -`authentication-service-0.4.40-linux-amd64.tar.xz` asset. The manually +`authentication-service-0.4.41-linux-amd64.tar.xz` asset. The manually dispatched release workflow runs the full test and validation suite twice, requires byte-identical packages, verifies a fixed and attested security scanner, publishes CycloneDX SBOMs and scan evidence, and publishes the diff --git a/server/auth_server.go b/server/auth_server.go index 8c0fb10..fb988bf 100644 --- a/server/auth_server.go +++ b/server/auth_server.go @@ -1076,6 +1076,15 @@ func Reload(fromUpdate bool) (bool, error) { return false, nil } + if strings.EqualFold(authConfig.Provider, oidcProviderName) && + canApplyOIDCReloadWithoutInitialization( + authConfigInMemory, authConfig, provider != nil) { + log.Info("Applying OpenID Connect access-policy reload without provider initialization") + authConfigInMemory = authConfig + <-*refreshReqChannel + return false, nil + } + if err := prepareProviderConfig(&authConfig); err != nil { <-*refreshReqChannel return false, err diff --git a/server/config_update_policy.go b/server/config_update_policy.go index e2db702..20551e4 100644 --- a/server/config_update_policy.go +++ b/server/config_update_policy.go @@ -169,6 +169,19 @@ func planOIDCConfigUpdate(current model.AuthConfig, requested model.AuthConfig) }, nil } +// canApplyOIDCReloadWithoutInitialization keeps platform setting events from +// turning an access-policy-only save into a second provider initialization. +// The provider must already be live; startup, first enablement, provider +// switches, and identity-source changes continue through the full reload path. +func canApplyOIDCReloadWithoutInitialization(current model.AuthConfig, + requested model.AuthConfig, providerReady bool) bool { + if !providerReady { + return false + } + plan, err := planOIDCConfigUpdate(current, requested) + return err == nil && plan.SameProvider && !plan.RequiresProviderInitialization +} + func oidcIdentitySourceChanged(current model.OIDCConfig, requested model.OIDCConfig) bool { return current.WellKnownURL != requested.WellKnownURL || current.ClientID != requested.ClientID || diff --git a/server/config_update_policy_test.go b/server/config_update_policy_test.go index 7945601..4ce2d2a 100644 --- a/server/config_update_policy_test.go +++ b/server/config_update_policy_test.go @@ -31,6 +31,33 @@ func TestOIDCPolicyOnlyUpdateSkipsRecoveryAndProviderInitialization(t *testing.T } } +func TestOIDCPolicyOnlyReloadSkipsProviderInitialization(t *testing.T) { + current := oidcConfigForPolicyTest(true, "restricted", + oidcIdentity("oidc_user", "alice")) + policyOnly := current + policyOnly.AllowedIdentities = append(policyOnly.AllowedIdentities, + oidcIdentity("oidc_group", "operators")) + + if !canApplyOIDCReloadWithoutInitialization(current, policyOnly, true) { + t.Fatal("a live unchanged OIDC provider would be initialized for a policy-only reload") + } + if canApplyOIDCReloadWithoutInitialization(current, policyOnly, false) { + t.Fatal("startup skipped required OIDC provider initialization") + } + + sourceChange := policyOnly + sourceChange.OIDCConfig.ClientID = "replacement-client" + if canApplyOIDCReloadWithoutInitialization(current, sourceChange, true) { + t.Fatal("an OIDC identity-source change skipped provider initialization") + } + + initialEnable := current + initialEnable.Enabled = false + if canApplyOIDCReloadWithoutInitialization(initialEnable, current, true) { + t.Fatal("initial OIDC enablement skipped provider initialization") + } +} + func TestExpiredLocalRecoveryOnlyBlocksIdentitySourceChanges(t *testing.T) { now := time.UnixMilli(1_800_000_000_000) expiredRecovery := map[string]string{