Architecture: - Extract reconcileNetworking into internal/reconcile package with 7 consumer-side interfaces - Add locked modifyState helper to fix state.yaml read-modify-write race condition - Extract CrowdSec and VPN handler groups with interfaces documenting dependency surface - Replace raw map[string]any YAML manipulation with typed AddDHCPStaticLease/RemoveDHCPStaticLease - Extract Cloudflare API functions into cfClient struct, eliminating repeated auth boilerplate Complexity reduction: - haproxy.GenerateWithOpts: 50 → 6 (extracted 8 focused helpers) - reconcile.Reconcile: 42 → 11 (extracted buildRoutes, buildDNSEntries, writeHAProxyConfig) - AutheliaUpdateConfig: 25 → 10 (extracted enableAuthelia/disableAuthelia) Test coverage improvements: - reconcile: 4.5% → 56.8% (stub-based tests for route building, DNS entries, orchestration) - dnsfilter: 10.7% → 45.6% (Manager.Compile, AddList, ToggleList, custom entries) - config: 67.3% → 89.8% (DHCP static lease mutation tests)
220 lines
5.8 KiB
Go
220 lines
5.8 KiB
Go
package authelia
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/wild-cloud/wild-central/internal/storage"
|
|
)
|
|
|
|
// OIDCClient represents a registered OIDC client application
|
|
type OIDCClient struct {
|
|
ID string `yaml:"client_id" json:"clientId"`
|
|
Name string `yaml:"client_name" json:"clientName"`
|
|
Secret string `yaml:"client_secret" json:"clientSecret,omitempty"` // pbkdf2 hash in config, cleartext on create response
|
|
RedirectURIs []string `yaml:"redirect_uris" json:"redirectUris"`
|
|
Scopes []string `yaml:"scopes" json:"scopes"`
|
|
Policy string `yaml:"authorization_policy" json:"authorizationPolicy"`
|
|
ConsentMode string `yaml:"consent_mode,omitempty" json:"consentMode,omitempty"`
|
|
}
|
|
|
|
// OIDCClientUpdate holds partial updates for an OIDC client
|
|
type OIDCClientUpdate struct {
|
|
Name *string `json:"clientName,omitempty"`
|
|
RedirectURIs []string `json:"redirectUris,omitempty"`
|
|
Scopes []string `json:"scopes,omitempty"`
|
|
Policy *string `json:"authorizationPolicy,omitempty"`
|
|
ConsentMode *string `json:"consentMode,omitempty"`
|
|
}
|
|
|
|
// oidcClientsFile is the top-level structure of the OIDC clients file
|
|
type oidcClientsFile struct {
|
|
Clients []OIDCClient `yaml:"clients"`
|
|
}
|
|
|
|
// ListOIDCClients returns all registered OIDC clients (secrets redacted)
|
|
func (m *Manager) ListOIDCClients() ([]OIDCClient, error) {
|
|
clients, err := m.readOIDCClients()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Redact secrets for listing
|
|
for i := range clients {
|
|
clients[i].Secret = ""
|
|
}
|
|
return clients, nil
|
|
}
|
|
|
|
// ReadOIDCClients returns all clients with secrets (for config generation)
|
|
func (m *Manager) ReadOIDCClients() ([]OIDCClient, error) {
|
|
return m.readOIDCClients()
|
|
}
|
|
|
|
// AddOIDCClient adds a new OIDC client. Returns the cleartext secret (shown once).
|
|
func (m *Manager) AddOIDCClient(client OIDCClient) (cleartextSecret string, err error) {
|
|
if client.ID == "" {
|
|
return "", fmt.Errorf("client_id is required")
|
|
}
|
|
if client.Name == "" {
|
|
return "", fmt.Errorf("client_name is required")
|
|
}
|
|
if len(client.RedirectURIs) == 0 {
|
|
return "", fmt.Errorf("at least one redirect_uri is required")
|
|
}
|
|
|
|
lockPath := m.configPath() + ".lock"
|
|
err = storage.WithLock(lockPath, func() error {
|
|
clients, readErr := m.readOIDCClients()
|
|
if readErr != nil {
|
|
return readErr
|
|
}
|
|
|
|
for _, c := range clients {
|
|
if c.ID == client.ID {
|
|
return fmt.Errorf("client %q already exists", client.ID)
|
|
}
|
|
}
|
|
|
|
// Generate client secret
|
|
secret, genErr := GenerateSecret(32)
|
|
if genErr != nil {
|
|
return fmt.Errorf("generating client secret: %w", genErr)
|
|
}
|
|
cleartextSecret = secret
|
|
|
|
// Hash for storage
|
|
client.Secret = hashClientSecret(secret)
|
|
|
|
// Default scopes
|
|
if len(client.Scopes) == 0 {
|
|
client.Scopes = []string{"openid", "profile", "email", "groups"}
|
|
}
|
|
if client.Policy == "" {
|
|
client.Policy = "one_factor"
|
|
}
|
|
|
|
return m.writeOIDCClients(append(clients, client))
|
|
})
|
|
return
|
|
}
|
|
|
|
// UpdateOIDCClient applies partial updates to an OIDC client
|
|
func (m *Manager) UpdateOIDCClient(clientID string, updates OIDCClientUpdate) error {
|
|
lockPath := m.configPath() + ".lock"
|
|
return storage.WithLock(lockPath, func() error {
|
|
clients, err := m.readOIDCClients()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
found := false
|
|
for i, c := range clients {
|
|
if c.ID == clientID {
|
|
if updates.Name != nil {
|
|
clients[i].Name = *updates.Name
|
|
}
|
|
if updates.RedirectURIs != nil {
|
|
clients[i].RedirectURIs = updates.RedirectURIs
|
|
}
|
|
if updates.Scopes != nil {
|
|
clients[i].Scopes = updates.Scopes
|
|
}
|
|
if updates.Policy != nil {
|
|
clients[i].Policy = *updates.Policy
|
|
}
|
|
if updates.ConsentMode != nil {
|
|
clients[i].ConsentMode = *updates.ConsentMode
|
|
}
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("client %q not found", clientID)
|
|
}
|
|
|
|
return m.writeOIDCClients(clients)
|
|
})
|
|
}
|
|
|
|
// DeleteOIDCClient removes an OIDC client by ID
|
|
func (m *Manager) DeleteOIDCClient(clientID string) error {
|
|
lockPath := m.configPath() + ".lock"
|
|
return storage.WithLock(lockPath, func() error {
|
|
clients, err := m.readOIDCClients()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var filtered []OIDCClient
|
|
found := false
|
|
for _, c := range clients {
|
|
if c.ID == clientID {
|
|
found = true
|
|
continue
|
|
}
|
|
filtered = append(filtered, c)
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("client %q not found", clientID)
|
|
}
|
|
|
|
return m.writeOIDCClients(filtered)
|
|
})
|
|
}
|
|
|
|
// OIDCClientCount returns the number of registered OIDC clients
|
|
func (m *Manager) OIDCClientCount() int {
|
|
clients, err := m.readOIDCClients()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return len(clients)
|
|
}
|
|
|
|
// readOIDCClients reads OIDC clients from the dedicated clients file
|
|
func (m *Manager) readOIDCClients() ([]OIDCClient, error) {
|
|
data, err := os.ReadFile(m.oidcClientsPath())
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("reading oidc clients: %w", err)
|
|
}
|
|
|
|
var f oidcClientsFile
|
|
if err := yaml.Unmarshal(data, &f); err != nil {
|
|
return nil, fmt.Errorf("parsing oidc clients: %w", err)
|
|
}
|
|
|
|
return f.Clients, nil
|
|
}
|
|
|
|
// writeOIDCClients writes the OIDC clients to the dedicated clients file
|
|
func (m *Manager) writeOIDCClients(clients []OIDCClient) error {
|
|
f := oidcClientsFile{Clients: clients}
|
|
data, err := yaml.Marshal(f)
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling oidc clients: %w", err)
|
|
}
|
|
|
|
tmpPath := m.oidcClientsPath() + ".tmp"
|
|
if err := os.WriteFile(tmpPath, data, 0600); err != nil {
|
|
return fmt.Errorf("writing temp oidc clients: %w", err)
|
|
}
|
|
if err := os.Rename(tmpPath, m.oidcClientsPath()); err != nil {
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("installing oidc clients: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// hashClientSecret wraps the secret with $plaintext$ prefix.
|
|
// Authelia accepts this format and handles hashing internally.
|
|
func hashClientSecret(secret string) string {
|
|
return "$plaintext$" + secret
|
|
}
|