Implement KeyCape provider and service identity contracts
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 25s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 25s
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02e3f-7301-7622-9be1-12e5f352881c
This commit is contained in:
parent
cdfb046b80
commit
efce3e9331
15 changed files with 579 additions and 26 deletions
|
|
@ -29,16 +29,17 @@ type Config struct {
|
|||
|
||||
// ClientConfig is a static OIDC client registration.
|
||||
type ClientConfig struct {
|
||||
ClientID string `yaml:"clientId"`
|
||||
DisplayName string `yaml:"displayName"`
|
||||
RedirectURIs []string `yaml:"redirectUris"`
|
||||
AllowedScopes []string `yaml:"allowedScopes"`
|
||||
GrantTypes []string `yaml:"grantTypes"`
|
||||
ClientType string `yaml:"clientType"` // "confidential" | "public"
|
||||
SecretRef string `yaml:"secretRef,omitempty"`
|
||||
ServiceSubject string `yaml:"serviceSubject,omitempty"`
|
||||
Tenant string `yaml:"tenant,omitempty"`
|
||||
ClientID string `yaml:"clientId"`
|
||||
DisplayName string `yaml:"displayName"`
|
||||
RedirectURIs []string `yaml:"redirectUris"`
|
||||
AllowedScopes []string `yaml:"allowedScopes"`
|
||||
GrantTypes []string `yaml:"grantTypes"`
|
||||
ClientType string `yaml:"clientType"` // "confidential" | "public"
|
||||
SecretRef string `yaml:"secretRef,omitempty"`
|
||||
ServiceSubject string `yaml:"serviceSubject,omitempty"`
|
||||
Tenant string `yaml:"tenant,omitempty"`
|
||||
Roles []string `yaml:"roles,omitempty"`
|
||||
TokenLifetime string `yaml:"tokenLifetime,omitempty"`
|
||||
MFARequired *bool `yaml:"mfaRequired,omitempty"`
|
||||
RegistrationURL string `yaml:"registrationUrl,omitempty"`
|
||||
EnrollmentURL string `yaml:"enrollmentUrl,omitempty"`
|
||||
|
|
|
|||
|
|
@ -337,6 +337,78 @@ func TestValidate_MissingPrivateKeyPEM(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if len(cfg.Clients) != 2 {
|
||||
t.Fatalf("service client examples: want 2, got %d", len(cfg.Clients))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Env var loading test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ValidateConfig validates a loaded Config and returns a list of human-readable
|
||||
|
|
@ -56,6 +57,16 @@ func ValidateConfig(cfg *Config) []string {
|
|||
if c.ServiceSubject == "" || c.Tenant == "" {
|
||||
errs = append(errs, prefix+": client_credentials requires serviceSubject and tenant")
|
||||
}
|
||||
if c.TokenLifetime != "" {
|
||||
lifetime, err := time.ParseDuration(c.TokenLifetime)
|
||||
if err != nil {
|
||||
errs = append(errs, prefix+": tokenLifetime must be a valid duration")
|
||||
} else if lifetime < time.Minute || lifetime > time.Hour {
|
||||
errs = append(errs, prefix+": tokenLifetime must be between 1m and 1h")
|
||||
}
|
||||
}
|
||||
} else if c.TokenLifetime != "" {
|
||||
errs = append(errs, prefix+": tokenLifetime is only supported for client_credentials clients")
|
||||
}
|
||||
// Warn about wildcard redirect URIs (they are blocked at runtime anyway).
|
||||
for _, uri := range c.RedirectURIs {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue