key-cape/src/internal/config/validate.go

111 lines
3.6 KiB
Go
Raw Normal View History

package config
import (
"fmt"
"net/url"
"strings"
)
// ValidateConfig validates a loaded Config and returns a list of human-readable
// error messages. An empty slice means the config is valid.
// Called at startup — the server must exit 1 if any errors are returned.
func ValidateConfig(cfg *Config) []string {
var errs []string
// Issuer must be a valid URL with an http(s) scheme.
if cfg.Issuer == "" {
errs = append(errs, "issuer: must not be empty")
} else {
u, err := url.Parse(cfg.Issuer)
if err != nil || u.Scheme == "" || u.Host == "" {
errs = append(errs, fmt.Sprintf("issuer: %q is not a valid URL (must include scheme and host)", cfg.Issuer))
} else if u.Scheme != "http" && u.Scheme != "https" {
errs = append(errs, fmt.Sprintf("issuer: scheme must be http or https, got %q", u.Scheme))
}
}
// Port must be in the valid TCP range.
if cfg.Port < 1 || cfg.Port > 65535 {
errs = append(errs, fmt.Sprintf("port: must be between 1 and 65535, got %d", cfg.Port))
}
// At least one client must be registered.
if len(cfg.Clients) == 0 {
errs = append(errs, "clients: at least one client must be defined")
}
// Each client must have at least one redirect URI and a non-empty clientId.
for i, c := range cfg.Clients {
prefix := fmt.Sprintf("clients[%d] (%s)", i, c.ClientID)
if c.ClientID == "" {
prefix = fmt.Sprintf("clients[%d]", i)
errs = append(errs, prefix+": clientId must not be empty")
}
hasAuthorizationCode := contains(c.GrantTypes, "authorization_code")
hasClientCredentials := contains(c.GrantTypes, "client_credentials")
if (hasAuthorizationCode || !hasClientCredentials) && len(c.RedirectURIs) == 0 {
errs = append(errs, prefix+": redirect_uri: at least one redirectUri must be registered")
}
if hasClientCredentials {
if c.ClientType != "confidential" {
errs = append(errs, prefix+": client_credentials requires clientType confidential")
}
if !strings.HasPrefix(c.SecretRef, "env:") {
errs = append(errs, prefix+": client_credentials requires an env: secretRef")
}
if c.ServiceSubject == "" || c.Tenant == "" {
errs = append(errs, prefix+": client_credentials requires serviceSubject and tenant")
}
}
// Warn about wildcard redirect URIs (they are blocked at runtime anyway).
for _, uri := range c.RedirectURIs {
if strings.ContainsAny(uri, "*?") {
errs = append(errs, prefix+fmt.Sprintf(": redirect_uri %q must not contain wildcards", uri))
}
}
if c.RegistrationURL != "" {
if err := validateHandoffURL(c.RegistrationURL); err != nil {
errs = append(errs, prefix+": registrationUrl: "+err.Error())
}
}
if c.EnrollmentURL != "" {
if err := validateHandoffURL(c.EnrollmentURL); err != nil {
errs = append(errs, prefix+": enrollmentUrl: "+err.Error())
}
}
}
// Private key PEM path must be provided (existence is checked at startup).
if cfg.PrivateKeyPEM == "" {
errs = append(errs, "privateKeyPem: path must not be empty")
}
return errs
}
func contains(values []string, wanted string) bool {
for _, value := range values {
if value == wanted {
return true
}
}
return false
}
func validateHandoffURL(raw string) error {
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" || u.Host == "" {
return fmt.Errorf("%q is not an absolute URL", raw)
}
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("%q scheme must be http or https", raw)
}
if u.User != nil {
return fmt.Errorf("%q must not contain userinfo", raw)
}
if strings.ContainsAny(u.Host, "*?") || strings.ContainsAny(u.Path, "*") {
return fmt.Errorf("%q must not contain wildcards", raw)
}
return nil
}