Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ jobs:

{
printf '# PastureStack Authentication Service %s\n\n' "$RELEASE_TAG"
printf 'This release preserves an explicit empty OIDC allowlist on the platform API wire. It avoids the generated setting client omitting an empty value, so a confirmed unrestricted transition durably clears stale restricted identities. Access-only changes continue to skip discovery and provider reload; access expansion remains protected by a single-use MFA confirmation bound to the operator and canonical request digest.\n\n'
printf 'This release makes the common OIDC site-access policy authoritative after the encrypted authentication configuration has been created. Authentication-service startup no longer replays absent legacy OIDC keys over a saved restricted or unrestricted policy, so access mode and allowlist values survive process and Server container restarts. The one-time legacy migration path remains available before the canonical configuration exists.\n\n'
printf 'It also preserves the previous explicit-empty allowlist wire contract: a confirmed unrestricted transition durably clears stale restricted identities. Access-only changes continue to skip discovery and provider reload; access expansion remains protected by a single-use MFA confirmation bound to the operator and canonical request digest.\n\n'
printf '## Immutable coordinates\n\n'
printf -- '- Source commit: `%s`\n' "$SOURCE_SHA"
printf -- '- Artifact SHA-256: `%s`\n\n' "$artifact_sha"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/security-release-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.39
VERSION_OVERRIDE: v0.4.40
steps:
- name: Check out candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -70,7 +70,7 @@ jobs:
}

run_ci
artifact="dist/artifacts/authentication-service-0.4.39-linux-amd64.tar.xz"
artifact="dist/artifacts/authentication-service-0.4.40-linux-amd64.tar.xz"
test -s "$artifact"
cp "$artifact" /tmp/authentication-service-first.tar.xz
rm -rf bin dist
Expand All @@ -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.39' >/dev/null
evidence/product/authentication-service --version | grep -F '0.4.40' >/dev/null
sha256sum "$artifact" > evidence/authentication-service.tar.xz.sha256
docker run --rm --entrypoint go \
--volume "$PWD:/work:ro" \
Expand Down
7 changes: 7 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ The empty allowlist must be present as an explicit `value: ""` field in the
platform setting update. Generated client omission rules must not turn the
clear operation into a no-op.

Legacy provider settings are imported only while no encrypted `auth.config`
object exists. After that migration boundary, the common access-policy
settings are authoritative: startup and restart must not copy absent legacy
OIDC keys over a saved access mode or allowlist. Both an explicit empty
unrestricted allowlist and a populated restricted/required allowlist must
round-trip across authentication-service and Server container restarts.

Operator lifecycle messages support `en-US` and `zh-TW`. Tokens, usernames,
groups, identity-provider data, OpenID Connect claims, SAML documents,
database settings, HTTP payloads, and protocol errors are not translated.
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ PastureStack is an independent community effort to preserve, audit, and moderniz

## Project status

The current compatibility release is `v0.4.39`. It retains the existing Ubuntu 26.04,
The current compatibility release is `v0.4.40`. 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,
Expand All @@ -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.39` separates OIDC identity-source changes from site-access
Release `v0.4.40` 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.
Expand All @@ -37,6 +37,12 @@ The unrestricted transition sends an explicit empty allowlist value on the
platform API wire. This prevents the generated client's `omitempty` behavior
from turning a requested clear into an omitted field and retaining stale
restricted identities in the database.
The legacy-settings importer now runs only before the encrypted `auth.config`
object exists. Once migration has completed, a process restart cannot replay
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.

Product-owned imports, executable names, CLI settings, client variables, and
operator messages use PastureStack naming.
Expand All @@ -55,9 +61,9 @@ make build
make package
```

Set `VERSION_OVERRIDE=v0.4.39` for the reviewed identity-security compatibility
Set `VERSION_OVERRIDE=v0.4.40` for the reviewed identity-security compatibility
release. Packaging produces the deterministic, versioned
`authentication-service-0.4.39-linux-amd64.tar.xz` asset. The manually
`authentication-service-0.4.40-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
Expand Down
34 changes: 28 additions & 6 deletions server/auth_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,18 @@ func readSettings(provider string) (map[string]string, error) {
return providerSettings, nil
}

func storedAuthConfigExists() (bool, error) {
if PlatformClient == nil {
return false, fmt.Errorf("platform API client is not configured")
}
filters := map[string]interface{}{"key": "auth.config"}
authColl, err := PlatformClient.GenericObject.List(&client.ListOpts{Filters: filters})
if err != nil {
return false, err
}
return len(authColl.Data) > 0, nil
}

func readCommonSettings(settings []string) (map[string]string, error) {
var dbSettings = make(map[string]string)
if PlatformClient == nil {
Expand Down Expand Up @@ -768,6 +780,20 @@ func localRecoveryReady(settings map[string]string, now time.Time) bool {

// UpgradeSettings upgrades the existing provider specific auth settings to the new generic settings used by this service
func UpgradeSettings() error {
// Legacy provider settings are a one-time migration source. Once the
// encrypted auth.config object exists, the common settings are authoritative
// and must not be overwritten on every process restart. OIDC has no legacy
// access-policy keys, so repeating this migration used to clear a valid
// restricted allowlist after an otherwise successful policy save.
stored, err := storedAuthConfigExists()
if err != nil {
return errors.Wrap(err, "UpgradeSettings: Could not inspect the stored authentication configuration")
}
if stored {
log.Info("Authentication configuration already migrated; skipping legacy settings upgrade")
return nil
}

//read the current provider
var settings []string
settings = append(settings, providerSetting)
Expand Down Expand Up @@ -828,17 +854,13 @@ func UpgradeCase() error {
}}

// check if GenericObject with key="auth.config" exists
filters := make(map[string]interface{})
filters["key"] = "auth.config"
authColl, err := PlatformClient.GenericObject.List(&client.ListOpts{
Filters: filters,
})
stored, err := storedAuthConfigExists()
if err != nil {
log.Errorf("Error getting the go 'auth.config', error: %v", err)
return err
}

if len(authColl.Data) > 0 {
if stored {
log.Info("Config stored")
return nil
}
Expand Down
43 changes: 43 additions & 0 deletions server/config_update_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,49 @@ func TestPolicyOnlyUpdateClearsStoredAllowlistWithoutDiscovery(t *testing.T) {
}
}

func TestUpgradeSettingsDoesNotReplayLegacyMigrationAfterCanonicalConfigExists(t *testing.T) {
genericObjectReads := 0
settingRequests := 0
var platformServer *httptest.Server
platformServer = httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Content-Type", "application/json")
switch {
case request.Method == http.MethodGet && request.URL.Path == "/v2-beta":
response.Header().Set("X-API-Schemas", platformServer.URL+"/v2-beta")
_, _ = fmt.Fprintf(response, `{"data":[{"id":"genericObject","type":"schema","pluralName":"genericObjects","collectionMethods":["GET"],"resourceMethods":["GET","PUT"],"links":{"collection":%q}},{"id":"setting","type":"schema","pluralName":"settings","collectionMethods":["GET"],"resourceMethods":["GET","PUT"],"links":{"collection":%q}}]}`,
platformServer.URL+"/v2-beta/genericObjects", platformServer.URL+"/v2-beta/settings")
case request.Method == http.MethodGet && request.URL.Path == "/v2-beta/genericObjects":
genericObjectReads++
_, _ = fmt.Fprint(response, `{"type":"collection","resourceType":"genericObject","data":[{"id":"1go1","type":"genericObject","key":"auth.config","name":"auth.config","kind":"authConfig","resourceData":{}}]}`)
case strings.HasPrefix(request.URL.Path, "/v2-beta/settings"):
settingRequests++
http.Error(response, "legacy settings must not be read after migration", http.StatusInternalServerError)
default:
t.Errorf("unexpected platform request %s %s", request.Method, request.URL.String())
http.Error(response, "unexpected request", http.StatusNotFound)
}
}))
defer platformServer.Close()

platformClient, err := newPlatformClient(platformServer.URL, "access", "secret")
if err != nil {
t.Fatal(err)
}
previousPlatformClient := PlatformClient
PlatformClient = platformClient
defer func() { PlatformClient = previousPlatformClient }()

if err := UpgradeSettings(); err != nil {
t.Fatal(err)
}
if genericObjectReads != 1 {
t.Fatalf("expected one canonical config lookup, got %d", genericObjectReads)
}
if settingRequests != 0 {
t.Fatalf("legacy settings were touched %d times after migration", settingRequests)
}
}

func oidcConfigForPolicyTest(enabled bool, accessMode string, identities ...client.Identity) model.AuthConfig {
return model.AuthConfig{
Provider: oidcProviderName,
Expand Down
Loading