2026-03-13 02:18:36 +01:00
|
|
|
package config_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"keycape/internal/config"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Helpers
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// writeTempFile creates a temporary file with the given content and returns its path.
|
|
|
|
|
func writeTempFile(t *testing.T, content string) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
f, err := os.CreateTemp(t.TempDir(), "keycape-test-*")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("create temp file: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := f.WriteString(content); err != nil {
|
|
|
|
|
t.Fatalf("write temp file: %v", err)
|
|
|
|
|
}
|
|
|
|
|
f.Close()
|
|
|
|
|
return f.Name()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validConfig returns a minimal valid Config for use in tests.
|
|
|
|
|
func validConfig(keyPath string) *config.Config {
|
|
|
|
|
return &config.Config{
|
|
|
|
|
Issuer: "https://auth.example.com",
|
|
|
|
|
Port: 8080,
|
|
|
|
|
TokenLifetime: "15m",
|
|
|
|
|
PrivateKeyPEM: keyPath,
|
|
|
|
|
Environment: "dev",
|
|
|
|
|
Clients: []config.ClientConfig{
|
|
|
|
|
{
|
|
|
|
|
ClientID: "test-app",
|
|
|
|
|
DisplayName: "Test App",
|
|
|
|
|
RedirectURIs: []string{"https://app.example.com/callback"},
|
|
|
|
|
ClientType: "public",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Load tests
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func TestLoad_ValidYAML(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "placeholder-key")
|
|
|
|
|
yaml := `
|
|
|
|
|
issuer: "https://auth.example.com"
|
|
|
|
|
port: 8080
|
|
|
|
|
tokenLifetime: "15m"
|
|
|
|
|
privateKeyPem: "` + keyPath + `"
|
|
|
|
|
environment: "dev"
|
|
|
|
|
clients:
|
|
|
|
|
- clientId: "demo"
|
|
|
|
|
displayName: "Demo"
|
|
|
|
|
redirectUris:
|
|
|
|
|
- "https://demo.example.com/cb"
|
|
|
|
|
clientType: "public"
|
|
|
|
|
`
|
|
|
|
|
cfgPath := writeTempFile(t, yaml)
|
|
|
|
|
|
|
|
|
|
cfg, err := config.Load(cfgPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Load: unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Issuer != "https://auth.example.com" {
|
|
|
|
|
t.Errorf("Issuer: want %q, got %q", "https://auth.example.com", cfg.Issuer)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Port != 8080 {
|
|
|
|
|
t.Errorf("Port: want 8080, got %d", cfg.Port)
|
|
|
|
|
}
|
|
|
|
|
if len(cfg.Clients) != 1 {
|
|
|
|
|
t.Errorf("Clients: want 1, got %d", len(cfg.Clients))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 17:03:01 +02:00
|
|
|
func TestLoad_AutheliaSplitURLs(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "placeholder-key")
|
|
|
|
|
yaml := `
|
|
|
|
|
issuer: "https://kc.example.com"
|
|
|
|
|
port: 8080
|
|
|
|
|
tokenLifetime: "15m"
|
|
|
|
|
privateKeyPem: "` + keyPath + `"
|
|
|
|
|
environment: "dev"
|
|
|
|
|
authelia:
|
|
|
|
|
baseURL: "http://authelia.sso.svc.cluster.local:9091"
|
|
|
|
|
browserBaseURL: "https://auth.example.com"
|
|
|
|
|
tokenBaseURL: "http://authelia.sso.svc.cluster.local:9091"
|
|
|
|
|
clientId: "keycape"
|
|
|
|
|
clientSecret: "secret"
|
|
|
|
|
redirectURI: "https://kc.example.com/authorize/callback"
|
|
|
|
|
clients:
|
|
|
|
|
- clientId: "netkingdom-bootstrap-console"
|
|
|
|
|
displayName: "NetKingdom Bootstrap Console"
|
|
|
|
|
redirectUris:
|
|
|
|
|
- "http://127.0.0.1:8876/oidc/callback"
|
|
|
|
|
- "http://localhost:8876/oidc/callback"
|
|
|
|
|
clientType: "public"
|
|
|
|
|
`
|
|
|
|
|
cfgPath := writeTempFile(t, yaml)
|
|
|
|
|
|
|
|
|
|
cfg, err := config.Load(cfgPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Load: unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Authelia.BaseURL != "http://authelia.sso.svc.cluster.local:9091" {
|
|
|
|
|
t.Errorf("Authelia.BaseURL: got %q", cfg.Authelia.BaseURL)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Authelia.BrowserBaseURL != "https://auth.example.com" {
|
|
|
|
|
t.Errorf("Authelia.BrowserBaseURL: got %q", cfg.Authelia.BrowserBaseURL)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Authelia.TokenBaseURL != "http://authelia.sso.svc.cluster.local:9091" {
|
|
|
|
|
t.Errorf("Authelia.TokenBaseURL: got %q", cfg.Authelia.TokenBaseURL)
|
|
|
|
|
}
|
|
|
|
|
if len(cfg.Clients) != 1 || cfg.Clients[0].ClientID != "netkingdom-bootstrap-console" {
|
|
|
|
|
t.Fatalf("bootstrap client not loaded: %+v", cfg.Clients)
|
|
|
|
|
}
|
|
|
|
|
if got := cfg.Clients[0].RedirectURIs; len(got) != 2 || got[0] != "http://127.0.0.1:8876/oidc/callback" {
|
|
|
|
|
t.Errorf("bootstrap redirect URIs not loaded: %+v", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 01:05:27 +02:00
|
|
|
func TestLoad_ClientMFAAndRegistrationURL(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "placeholder-key")
|
|
|
|
|
yaml := `
|
|
|
|
|
issuer: "https://kc.example.com"
|
|
|
|
|
port: 8080
|
|
|
|
|
tokenLifetime: "15m"
|
|
|
|
|
privateKeyPem: "` + keyPath + `"
|
|
|
|
|
environment: "dev"
|
|
|
|
|
clients:
|
|
|
|
|
- clientId: "coulomb-social"
|
|
|
|
|
displayName: "coulomb.social"
|
|
|
|
|
redirectUris:
|
|
|
|
|
- "https://coulomb.social/auth/callback/"
|
|
|
|
|
clientType: "public"
|
|
|
|
|
mfaRequired: false
|
|
|
|
|
registrationUrl: "https://users.example.com/register"
|
|
|
|
|
`
|
|
|
|
|
cfgPath := writeTempFile(t, yaml)
|
|
|
|
|
cfg, err := config.Load(cfgPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Load: unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(cfg.Clients) != 1 {
|
|
|
|
|
t.Fatalf("clients: got %d", len(cfg.Clients))
|
|
|
|
|
}
|
|
|
|
|
c := cfg.Clients[0]
|
|
|
|
|
if c.MFARequired == nil || *c.MFARequired {
|
|
|
|
|
t.Fatalf("mfaRequired: want false, got %+v", c.MFARequired)
|
|
|
|
|
}
|
|
|
|
|
if c.RegistrationURL != "https://users.example.com/register" {
|
|
|
|
|
t.Errorf("registrationUrl: got %q", c.RegistrationURL)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidate_InvalidRegistrationURL(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
cfg := validConfig(keyPath)
|
|
|
|
|
cfg.Clients[0].RegistrationURL = "javascript:alert(1)"
|
|
|
|
|
errs := config.ValidateConfig(cfg)
|
|
|
|
|
if !containsErr(errs, "registrationUrl") {
|
|
|
|
|
t.Errorf("expected registrationUrl error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 00:09:40 +02:00
|
|
|
func TestLoad_PrivacyIDEARequireForAll(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "placeholder-key")
|
|
|
|
|
yaml := `
|
|
|
|
|
issuer: "https://kc.example.com"
|
|
|
|
|
port: 8080
|
|
|
|
|
tokenLifetime: "15m"
|
|
|
|
|
privateKeyPem: "` + keyPath + `"
|
|
|
|
|
environment: "dev"
|
|
|
|
|
privacyidea:
|
|
|
|
|
baseURL: "http://privacyidea.mfa.svc.cluster.local:8080"
|
|
|
|
|
adminToken: "service-token"
|
|
|
|
|
realm: "coulomb"
|
|
|
|
|
requireForAll: true
|
|
|
|
|
clients:
|
|
|
|
|
- clientId: "netkingdom-bootstrap-console"
|
|
|
|
|
displayName: "NetKingdom Bootstrap Console"
|
|
|
|
|
redirectUris:
|
|
|
|
|
- "http://127.0.0.1:8876/oidc/callback"
|
|
|
|
|
clientType: "public"
|
|
|
|
|
`
|
|
|
|
|
cfgPath := writeTempFile(t, yaml)
|
|
|
|
|
|
|
|
|
|
cfg, err := config.Load(cfgPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Load: unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.PrivacyIDEA.Realm != "coulomb" {
|
|
|
|
|
t.Errorf("PrivacyIDEA.Realm: got %q", cfg.PrivacyIDEA.Realm)
|
|
|
|
|
}
|
|
|
|
|
if !cfg.PrivacyIDEA.RequireForAll {
|
|
|
|
|
t.Error("PrivacyIDEA.RequireForAll should load from YAML")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 00:28:33 +02:00
|
|
|
func TestLoad_LLDAPOrganisationalUnits(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "placeholder-key")
|
|
|
|
|
yaml := `
|
|
|
|
|
issuer: "https://kc.example.com"
|
|
|
|
|
port: 8080
|
|
|
|
|
tokenLifetime: "15m"
|
|
|
|
|
privateKeyPem: "` + keyPath + `"
|
|
|
|
|
environment: "dev"
|
|
|
|
|
lldap:
|
|
|
|
|
url: "ldap://lldap.sso.svc.cluster.local:3890"
|
|
|
|
|
bindDN: "uid=admin,ou=people,dc=netkingdom,dc=local"
|
|
|
|
|
bindPW: "secret"
|
|
|
|
|
baseDN: "dc=netkingdom,dc=local"
|
|
|
|
|
userOU: "ou=people"
|
|
|
|
|
groupOU: "ou=groups"
|
|
|
|
|
clients:
|
|
|
|
|
- clientId: "netkingdom-bootstrap-console"
|
|
|
|
|
displayName: "NetKingdom Bootstrap Console"
|
|
|
|
|
redirectUris:
|
|
|
|
|
- "http://127.0.0.1:8876/oidc/callback"
|
|
|
|
|
clientType: "public"
|
|
|
|
|
`
|
|
|
|
|
cfgPath := writeTempFile(t, yaml)
|
|
|
|
|
|
|
|
|
|
cfg, err := config.Load(cfgPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Load: unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.LLDAP.UserOU != "ou=people" {
|
|
|
|
|
t.Errorf("LLDAP.UserOU: got %q", cfg.LLDAP.UserOU)
|
|
|
|
|
}
|
|
|
|
|
if cfg.LLDAP.GroupOU != "ou=groups" {
|
|
|
|
|
t.Errorf("LLDAP.GroupOU: got %q", cfg.LLDAP.GroupOU)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 02:18:36 +01:00
|
|
|
func TestLoad_FileNotFound(t *testing.T) {
|
|
|
|
|
_, err := config.Load(filepath.Join(t.TempDir(), "nonexistent.yaml"))
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Error("Load: expected error for missing file, got nil")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLoad_InvalidYAML(t *testing.T) {
|
|
|
|
|
bad := writeTempFile(t, "not: valid: yaml: [[[")
|
|
|
|
|
_, err := config.Load(bad)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Error("Load: expected error for invalid YAML, got nil")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Validate tests
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func TestValidate_ValidConfig(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
errs := config.ValidateConfig(validConfig(keyPath))
|
|
|
|
|
if len(errs) != 0 {
|
|
|
|
|
t.Errorf("ValidateConfig: expected no errors, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidate_MissingIssuer(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
cfg := validConfig(keyPath)
|
|
|
|
|
cfg.Issuer = ""
|
|
|
|
|
errs := config.ValidateConfig(cfg)
|
|
|
|
|
if !containsErr(errs, "issuer") {
|
|
|
|
|
t.Errorf("expected issuer error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidate_InvalidIssuerURL(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
cfg := validConfig(keyPath)
|
|
|
|
|
cfg.Issuer = "not a url"
|
|
|
|
|
errs := config.ValidateConfig(cfg)
|
|
|
|
|
if !containsErr(errs, "issuer") {
|
|
|
|
|
t.Errorf("expected issuer URL error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidate_PortZero(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
cfg := validConfig(keyPath)
|
|
|
|
|
cfg.Port = 0
|
|
|
|
|
errs := config.ValidateConfig(cfg)
|
|
|
|
|
if !containsErr(errs, "port") {
|
|
|
|
|
t.Errorf("expected port error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidate_PortTooHigh(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
cfg := validConfig(keyPath)
|
|
|
|
|
cfg.Port = 70000
|
|
|
|
|
errs := config.ValidateConfig(cfg)
|
|
|
|
|
if !containsErr(errs, "port") {
|
|
|
|
|
t.Errorf("expected port error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidate_NoClients(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
cfg := validConfig(keyPath)
|
|
|
|
|
cfg.Clients = nil
|
|
|
|
|
errs := config.ValidateConfig(cfg)
|
|
|
|
|
if !containsErr(errs, "client") {
|
|
|
|
|
t.Errorf("expected client error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidate_ClientMissingRedirectURI(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
cfg := validConfig(keyPath)
|
|
|
|
|
cfg.Clients[0].RedirectURIs = nil
|
|
|
|
|
errs := config.ValidateConfig(cfg)
|
|
|
|
|
if !containsErr(errs, "redirect") {
|
|
|
|
|
t.Errorf("expected redirect_uri error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidate_MissingPrivateKeyPEM(t *testing.T) {
|
|
|
|
|
cfg := validConfig("")
|
|
|
|
|
cfg.PrivateKeyPEM = ""
|
|
|
|
|
errs := config.ValidateConfig(cfg)
|
|
|
|
|
if !containsErr(errs, "privateKeyPem") {
|
|
|
|
|
t.Errorf("expected privateKeyPem error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 13:10:13 +02:00
|
|
|
func TestValidate_ClientCredentialsTokenLifetime(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
cfg := validConfig(keyPath)
|
|
|
|
|
cfg.Clients[0] = config.ClientConfig{
|
|
|
|
|
ClientID: "service-client",
|
|
|
|
|
ClientType: "confidential",
|
|
|
|
|
GrantTypes: []string{"client_credentials"},
|
|
|
|
|
AllowedScopes: []string{"openbao:login"},
|
|
|
|
|
SecretRef: "env:SERVICE_CLIENT_SECRET",
|
|
|
|
|
ServiceSubject: "service:test",
|
|
|
|
|
Tenant: "tenant:coulomb",
|
|
|
|
|
TokenLifetime: "15m",
|
|
|
|
|
}
|
|
|
|
|
if errs := config.ValidateConfig(cfg); len(errs) != 0 {
|
|
|
|
|
t.Fatalf("valid per-client token lifetime rejected: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg.Clients[0].TokenLifetime = "90m"
|
|
|
|
|
if errs := config.ValidateConfig(cfg); !containsErr(errs, "between 1m and 1h") {
|
|
|
|
|
t.Fatalf("expected bounded tokenLifetime error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg.Clients[0].TokenLifetime = "not-a-duration"
|
|
|
|
|
if errs := config.ValidateConfig(cfg); !containsErr(errs, "valid duration") {
|
|
|
|
|
t.Fatalf("expected invalid tokenLifetime error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidate_PublicClientRejectsTokenLifetime(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
cfg := validConfig(keyPath)
|
|
|
|
|
cfg.Clients[0].TokenLifetime = "15m"
|
|
|
|
|
if errs := config.ValidateConfig(cfg); !containsErr(errs, "only supported for client_credentials") {
|
|
|
|
|
t.Fatalf("expected public-client tokenLifetime error, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestServiceClientExampleContracts(t *testing.T) {
|
|
|
|
|
cfg, err := config.Load(filepath.Join("..", "..", "..", "config", "service-clients.example.yaml"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("load service client examples: %v", err)
|
|
|
|
|
}
|
|
|
|
|
cfg.Issuer = "https://kc.coulomb.social"
|
|
|
|
|
cfg.Port = 8080
|
|
|
|
|
cfg.PrivateKeyPEM = writeTempFile(t, "key")
|
|
|
|
|
if errs := config.ValidateConfig(cfg); len(errs) != 0 {
|
|
|
|
|
t.Fatalf("service client examples must validate: %v", errs)
|
|
|
|
|
}
|
2026-09-05 00:41:17 +02:00
|
|
|
if len(cfg.Clients) != 4 {
|
|
|
|
|
t.Fatalf("service client examples: want 4, got %d", len(cfg.Clients))
|
2026-08-23 13:10:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
codingAgent := cfg.Clients[0]
|
|
|
|
|
if codingAgent.ClientID != "codex-railiance-platform" ||
|
|
|
|
|
codingAgent.ServiceSubject != "service:codex:railiance-platform" ||
|
|
|
|
|
codingAgent.Tenant != "tenant:coulomb" ||
|
|
|
|
|
codingAgent.TokenLifetime != "15m" {
|
|
|
|
|
t.Fatalf("coding-agent contract drifted: %+v", codingAgent)
|
|
|
|
|
}
|
|
|
|
|
if len(codingAgent.Roles) != 1 || codingAgent.Roles[0] != "coding-agent" ||
|
|
|
|
|
len(codingAgent.AllowedScopes) != 1 || codingAgent.AllowedScopes[0] != "openbao:login" {
|
|
|
|
|
t.Fatalf("coding-agent authorization contract drifted: %+v", codingAgent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
secretsEngine := cfg.Clients[1]
|
|
|
|
|
if secretsEngine.ClientID != "secrets-engine-openbao" ||
|
|
|
|
|
secretsEngine.ServiceSubject != "service:secrets-engine" ||
|
|
|
|
|
secretsEngine.TokenLifetime != "15m" {
|
|
|
|
|
t.Fatalf("secrets-engine contract drifted: %+v", secretsEngine)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 13:16:21 +02:00
|
|
|
func TestDevConfigOpenBaoAdminAdmitsOperatorTunnelCallback(t *testing.T) {
|
|
|
|
|
cfg, err := config.Load(filepath.Join("..", "..", "..", "config", "dev-config.yaml"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("load dev config: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if errs := config.ValidateConfig(cfg); len(errs) != 0 {
|
|
|
|
|
t.Fatalf("dev config must validate: %v", errs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const callback = "http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback"
|
|
|
|
|
for _, client := range cfg.Clients {
|
|
|
|
|
if client.ClientID != "openbao-admin" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, redirectURI := range client.RedirectURIs {
|
|
|
|
|
if redirectURI == callback {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
t.Fatalf("openbao-admin redirectUris missing exact operator tunnel callback %q", callback)
|
|
|
|
|
}
|
|
|
|
|
t.Fatal("dev config missing openbao-admin client")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 02:18:36 +01:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Env var loading test
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func TestLoad_FromEnvVar(t *testing.T) {
|
|
|
|
|
keyPath := writeTempFile(t, "key")
|
|
|
|
|
yaml := `
|
|
|
|
|
issuer: "https://auth.example.com"
|
|
|
|
|
port: 9090
|
|
|
|
|
tokenLifetime: "30m"
|
|
|
|
|
privateKeyPem: "` + keyPath + `"
|
|
|
|
|
environment: "dev"
|
|
|
|
|
clients:
|
|
|
|
|
- clientId: "env-app"
|
|
|
|
|
displayName: "Env App"
|
|
|
|
|
redirectUris:
|
|
|
|
|
- "https://env.example.com/cb"
|
|
|
|
|
clientType: "public"
|
|
|
|
|
`
|
|
|
|
|
cfgPath := writeTempFile(t, yaml)
|
|
|
|
|
t.Setenv("KEYCAPE_CONFIG", cfgPath)
|
|
|
|
|
|
|
|
|
|
// Load with empty path triggers env var lookup.
|
|
|
|
|
cfg, err := config.Load("")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Load with env var: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if cfg.Port != 9090 {
|
|
|
|
|
t.Errorf("Port: want 9090, got %d", cfg.Port)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Helper
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
func containsErr(errs []string, substring string) bool {
|
|
|
|
|
for _, e := range errs {
|
|
|
|
|
for i := 0; i <= len(e)-len(substring); i++ {
|
|
|
|
|
if e[i:i+len(substring)] == substring {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-09-05 00:41:17 +02:00
|
|
|
|
|
|
|
|
func TestValidateConfigAudience(t *testing.T) {
|
|
|
|
|
for _, audience := range []string{"", "approval-engine", "https://api.example.com"} {
|
|
|
|
|
cfg := validConfig("key.pem")
|
|
|
|
|
cfg.Clients[0].Audience = audience
|
|
|
|
|
if errs := config.ValidateConfig(cfg); len(errs) != 0 {
|
|
|
|
|
t.Fatalf("valid audience %q: %v", audience, errs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, audience := range []string{" ", "approval-engine other", "approval-engine\n"} {
|
|
|
|
|
cfg := validConfig("key.pem")
|
|
|
|
|
cfg.Clients[0].Audience = audience
|
|
|
|
|
if errs := config.ValidateConfig(cfg); len(errs) == 0 {
|
|
|
|
|
t.Fatalf("accepted audience %q", audience)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-08 08:59:40 +02:00
|
|
|
|
|
|
|
|
// tenant_roles is opt-in, but a configured cache source must be usable: it sits
|
|
|
|
|
// on the synchronous token-issuance path (KEY-WP-0024).
|
|
|
|
|
func TestValidate_TenantEngine(t *testing.T) {
|
|
|
|
|
cases := map[string]struct {
|
|
|
|
|
engine config.TenantEngineConfig
|
|
|
|
|
valid bool
|
|
|
|
|
}{
|
|
|
|
|
"absent is valid": {config.TenantEngineConfig{}, true},
|
|
|
|
|
"http url": {config.TenantEngineConfig{BaseURL: "http://tenant-engine:8080"}, true},
|
|
|
|
|
"https url and timeout": {config.TenantEngineConfig{BaseURL: "https://tenant-engine", Timeout: "2s"}, true},
|
|
|
|
|
"not a url": {config.TenantEngineConfig{BaseURL: "tenant-engine"}, false},
|
|
|
|
|
"wrong scheme": {config.TenantEngineConfig{BaseURL: "ldap://tenant-engine"}, false},
|
|
|
|
|
"unparseable timeout": {config.TenantEngineConfig{BaseURL: "http://t", Timeout: "soon"}, false},
|
|
|
|
|
"zero timeout": {config.TenantEngineConfig{BaseURL: "http://t", Timeout: "0s"}, false},
|
|
|
|
|
"timeout far too long": {config.TenantEngineConfig{BaseURL: "http://t", Timeout: "5m"}, false},
|
|
|
|
|
// A timeout with no base URL means someone expected the claim to be on.
|
|
|
|
|
"timeout without url": {config.TenantEngineConfig{Timeout: "2s"}, false},
|
|
|
|
|
}
|
|
|
|
|
for name, tc := range cases {
|
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
|
cfg := validConfig(writeTempFile(t, "key"))
|
|
|
|
|
cfg.TenantEngine = tc.engine
|
|
|
|
|
errs := config.ValidateConfig(cfg)
|
|
|
|
|
if tc.valid && len(errs) != 0 {
|
|
|
|
|
t.Fatalf("expected valid, got %v", errs)
|
|
|
|
|
}
|
|
|
|
|
if !tc.valid && len(errs) == 0 {
|
|
|
|
|
t.Fatal("expected a validation error")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|