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>
112 lines
3.3 KiB
Go
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
|
|
}
|