mirror of
https://github.com/versity/versitygw.git
synced 2026-09-22 16:04:15 +00:00
`AssumeRoleWithWebIdentity` always fetched a provider's discovery document from `<provider url>/.well-known/openid-configuration`, so an identity provider that issues tokens naming a public issuer while serving its metadata and keys on a cluster-internal path could not be used: reaching it meant relaxing the endpoint checks for every registered provider. `--oidc-discovery-url` moves that one fetch to an operator-named endpoint, which is how keys can be looked up over an optimized private path while the tokens themselves stay verifiable from the public internet against the issuer alone, as the JWT spec requires. The flag takes `<provider url>=<discovery url>` pairs, can be repeated once per provider, and is also read from `VGW_IAM_OIDC_DISCOVERY_URLS` as a comma-separated list; the Helm chart exposes the same list as `iamServer.oidc.discoveryUrls`. The discovery URL is fetched exactly as written, so it must carry the `/.well-known/openid-configuration` path when the provider serves it there. A malformed pair is rejected at startup rather than at the first assume-role call. Only the fetch moves. The provider URL is still what a token's `iss` claim is matched against, the fetched document's own `issuer` field must still equal it, and the key set still comes from the `jwks_uri` that document publishes. A configured discovery endpoint is named by the operator at startup rather than by a request, so it and the `jwks_uri` it publishes waive the private-address check for that provider's fetch chain only, without `--oidc-allow-private-endpoints` and its far broader effect on every other provider. Transport rules are unchanged: a plaintext discovery URL still requires `--oidc-allow-insecure-transport`. Thumbprint auto-fetch follows the override and pins the discovery endpoint's certificate chain, since that is the host every later fetch is verified against.
150 lines
4.9 KiB
Go
150 lines
4.9 KiB
Go
// Copyright 2026 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package iamapi
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/versity/versitygw/iamapi/storage"
|
|
)
|
|
|
|
// TestOIDCConfigParseDiscoveryURLs covers the "<provider url>=<discovery
|
|
// url>" pairs the CLI and Helm chart pass through: a malformed pair fails at
|
|
// startup rather than at the first AssumeRoleWithWebIdentity, and a valid one
|
|
// is keyed by the provider's stored Url so the lookup at fetch time hits.
|
|
func TestOIDCConfigParseDiscoveryURLs(t *testing.T) {
|
|
const discoveryURL = "https://oidc.oidc-ns/.well-known/openid-configuration"
|
|
|
|
tests := []struct {
|
|
name string
|
|
pairs []string
|
|
insecure bool
|
|
private bool
|
|
want map[string]string
|
|
}{
|
|
{
|
|
name: "https provider keyed scheme-stripped",
|
|
pairs: []string{"https://oidc.example.com=" + discoveryURL},
|
|
want: map[string]string{"oidc.example.com": discoveryURL},
|
|
},
|
|
{
|
|
name: "provider port survives into the key",
|
|
pairs: []string{"https://oidc.example.com:8443=" + discoveryURL},
|
|
private: true,
|
|
want: map[string]string{"oidc.example.com:8443": discoveryURL},
|
|
},
|
|
{
|
|
name: "provider port without private endpoints",
|
|
pairs: []string{"https://oidc.example.com:8443=" + discoveryURL},
|
|
},
|
|
{
|
|
name: "provider path containing =",
|
|
pairs: []string{"https://oidc.example.com/tenant=abc=" + discoveryURL},
|
|
want: map[string]string{"oidc.example.com/tenant=abc": discoveryURL},
|
|
},
|
|
{
|
|
name: "discovery url query containing =",
|
|
pairs: []string{"https://oidc.example.com=" + discoveryURL + "?tenant=abc"},
|
|
want: map[string]string{"oidc.example.com": discoveryURL + "?tenant=abc"},
|
|
},
|
|
{
|
|
name: "http provider keeps its scheme",
|
|
pairs: []string{"http://127.0.0.1:8080=" + discoveryURL},
|
|
insecure: true,
|
|
private: true,
|
|
want: map[string]string{"http://127.0.0.1:8080": discoveryURL},
|
|
},
|
|
{
|
|
name: "plaintext discovery url with insecure transport",
|
|
pairs: []string{"https://oidc.example.com=http://127.0.0.1:8080/.well-known/openid-configuration"},
|
|
insecure: true,
|
|
want: map[string]string{"oidc.example.com": "http://127.0.0.1:8080/.well-known/openid-configuration"},
|
|
},
|
|
{
|
|
name: "plaintext discovery url without insecure transport",
|
|
pairs: []string{"https://oidc.example.com=http://127.0.0.1:8080/.well-known/openid-configuration"},
|
|
},
|
|
{
|
|
name: "no separator",
|
|
pairs: []string{"https://oidc.example.com"},
|
|
},
|
|
{
|
|
name: "empty discovery url",
|
|
pairs: []string{"https://oidc.example.com="},
|
|
},
|
|
{
|
|
name: "provider url without a scheme",
|
|
pairs: []string{"oidc.example.com=" + discoveryURL},
|
|
},
|
|
{
|
|
name: "discovery url without a scheme",
|
|
pairs: []string{"https://oidc.example.com=oidc.oidc-ns"},
|
|
},
|
|
{
|
|
name: "provider url without a host",
|
|
pairs: []string{"https://=" + discoveryURL},
|
|
},
|
|
{
|
|
name: "provider url with an invalid host",
|
|
pairs: []string{"https://bad host=" + discoveryURL},
|
|
},
|
|
{
|
|
name: "discovery url without a host",
|
|
pairs: []string{"https://oidc.example.com=https://"},
|
|
},
|
|
{
|
|
name: "discovery url with an invalid host",
|
|
pairs: []string{"https://oidc.example.com=https://bad host/.well-known/openid-configuration"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := OIDCConfig{DiscoveryURLs: tt.pairs, AllowInsecureTransport: tt.insecure, AllowPrivateEndpoints: tt.private}
|
|
err := cfg.parseDiscoveryURLs()
|
|
if tt.want == nil {
|
|
if err == nil {
|
|
t.Fatalf("parseDiscoveryURLs(%q) = nil, want an error", tt.pairs)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("parseDiscoveryURLs(%q): %v", tt.pairs, err)
|
|
}
|
|
got := cfg.endpointPolicy().DiscoveryURLs
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("DiscoveryURLs = %v, want %v", got, tt.want)
|
|
}
|
|
for provider, want := range tt.want {
|
|
if got[provider] != want {
|
|
t.Errorf("DiscoveryURLs[%q] = %q, want %q", provider, got[provider], want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNewRejectsInvalidDiscoveryURLs confirms the parse runs at construction
|
|
// time, so a bad flag value never reaches a running server.
|
|
func TestNewRejectsInvalidDiscoveryURLs(t *testing.T) {
|
|
store, err := storage.New(storage.Config{Dir: t.TempDir()})
|
|
if err != nil {
|
|
t.Fatalf("storage.New: %v", err)
|
|
}
|
|
if _, err := New(store, testRoot, WithQuiet(), WithOIDCDiscoveryURLs([]string{"not-a-pair"})); err == nil {
|
|
t.Fatal("New with a malformed discovery url = nil error, want a failure")
|
|
}
|
|
}
|