Files
edgeguard-native/internal/services/oidc/settings.go
Debian 02096c8ad8 chore(lint): golangci-lint --fix — misspell + staticcheck-Autofixes (Backlog 142→~98)
Erster Schritt des golangci-lint-Rollouts (non-blocking): 44 misspell + 4
staticcheck automatisch behoben (32 Dateien, nur Tippfehler/mechanisch).
build+test grün. Kein Runtime-Change → kein Deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 23:25:22 +02:00

112 lines
3.3 KiB
Go

// Package oidc kapselt die OIDC/Keycloak-SSO-Configuration (Singleton-
// Settings + verschlüsseltes Client-Secret) und einen lazy aufgebauten
// OIDC-Provider/Verifier. Login-Flow-State ist stateless (signiertes
// Cookie im Handler), daher hält dieses Paket keinen Request-State.
package oidc
import (
"context"
"errors"
"github.com/jackc/pgx/v5/pgxpool"
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/secrets"
)
// ErrDisabled signalisiert, dass OIDC nicht aktiviert/konfiguriert ist.
var ErrDisabled = errors.New("oidc: not enabled")
// Repo liest/schreibt die oidc_settings-Singleton-Row und ver-/entschlüsselt
// das Client-Secret via secrets.Box.
type Repo struct {
pool *pgxpool.Pool
box *secrets.Box
}
func New(pool *pgxpool.Pool, box *secrets.Box) *Repo {
return &Repo{pool: pool, box: box}
}
// Get liefert die Settings (client_secret_enc als Bytes, NULL → nil).
func (r *Repo) Get(ctx context.Context) (*models.OIDCSettings, error) {
var s models.OIDCSettings
if err := r.pool.QueryRow(ctx, `
SELECT id, enabled, issuer_url, client_id, client_secret_enc,
scopes, email_claim, button_label, created_at, updated_at
FROM oidc_settings WHERE id = 1`).Scan(
&s.ID, &s.Enabled, &s.IssuerURL, &s.ClientID, &s.ClientSecretEnc,
&s.Scopes, &s.EmailClaim, &s.ButtonLabel, &s.CreatedAt, &s.UpdatedAt,
); err != nil {
return nil, err
}
return &s, nil
}
// ClientSecret entschlüsselt das gespeicherte Client-Secret ("" wenn keins).
func (r *Repo) ClientSecret(ctx context.Context) (string, error) {
s, err := r.Get(ctx)
if err != nil {
return "", err
}
if len(s.ClientSecretEnc) == 0 {
return "", nil
}
pt, err := r.box.Open(s.ClientSecretEnc)
if err != nil {
return "", err
}
return string(pt), nil
}
// HasSecret meldet, ob ein Client-Secret hinterlegt ist (für die Admin-UI,
// ohne das Secret selbst preiszugeben).
func (r *Repo) HasSecret(ctx context.Context) (bool, error) {
var present bool
err := r.pool.QueryRow(ctx,
`SELECT client_secret_enc IS NOT NULL FROM oidc_settings WHERE id = 1`).Scan(&present)
return present, err
}
// UpdateInput beschreibt eine Settings-Änderung. ClientSecret nutzt
// write-only-Semantik: nil = unverändert, "" = löschen, sonst neu sealen.
type UpdateInput struct {
Enabled bool
IssuerURL string
ClientID string
Scopes string
EmailClaim string
ButtonLabel string
ClientSecret *string
}
// Update schreibt die Settings. Das Secret wird nur angefasst, wenn
// ClientSecret != nil.
func (r *Repo) Update(ctx context.Context, in UpdateInput) error {
if in.ClientSecret == nil {
_, err := r.pool.Exec(ctx, `
UPDATE oidc_settings
SET enabled=$1, issuer_url=$2, client_id=$3, scopes=$4,
email_claim=$5, button_label=$6, updated_at=NOW()
WHERE id=1`,
in.Enabled, in.IssuerURL, in.ClientID, in.Scopes, in.EmailClaim, in.ButtonLabel)
return err
}
var enc []byte
if *in.ClientSecret != "" {
sealed, err := r.box.Seal([]byte(*in.ClientSecret))
if err != nil {
return err
}
enc = sealed
}
_, err := r.pool.Exec(ctx, `
UPDATE oidc_settings
SET enabled=$1, issuer_url=$2, client_id=$3, scopes=$4,
email_claim=$5, button_label=$6, client_secret_enc=$7, updated_at=NOW()
WHERE id=1`,
in.Enabled, in.IssuerURL, in.ClientID, in.Scopes, in.EmailClaim, in.ButtonLabel, enc)
return err
}