Files
uopi/internal/config/config.go
T
Martino Ferrari c0f7e662be Testing
2026-06-24 01:39:15 +02:00

344 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/BurntSushi/toml"
)
type Config struct {
Server ServerConfig `toml:"server"`
Datasource DatasourceConfig `toml:"datasource"`
Audit AuditConfig `toml:"audit"`
UI UIConfig `toml:"ui"`
// Groups are named sets of users, referenced by panel sharing rules.
Groups []GroupDef `toml:"groups"`
}
// UIConfig carries client-side presentation defaults sent to the browser at
// startup (via /api/v1/me).
type UIConfig struct {
// DefaultZoom is the base UI scale multiplier applied when a browser has no
// per-machine zoom override saved. Useful to enlarge the UI by default on
// HiDPI screens whose OS scaling is left at 100% (where the browser reports
// devicePixelRatio=1 and the UI would otherwise render small). The in-app
// A+/A control still overrides it locally. 0 or unset means 1.0 (no scaling).
DefaultZoom float64 `toml:"default_zoom"`
}
// AuditConfig controls the audit trail. When enabled, every user and automated
// action that could affect the controlled system (signal writes, control-logic
// changes) is recorded to a SQLite database for later review by audit staff. Who
// may *view* the log is governed by the auditor role (see GroupDef).
type AuditConfig struct {
Enabled bool `toml:"enabled"`
DBPath string `toml:"db_path"` // SQLite file; default {storage_dir}/audit.db
}
// GroupDef defines one access group as [[groups]] in the config file. Each group
// has an optional parent (for nesting) and lists its members by role. Roles form
// a cumulative ladder: viewer < operator < logiceditor < auditor < admin. A user
// listed in several role buckets keeps the highest. The built-in "public" group
// (every user is an implicit viewer member) may be configured by name to raise
// specific users globally.
type GroupDef struct {
Name string `toml:"name"`
Parent string `toml:"parent"`
Viewers []string `toml:"viewers"`
Operators []string `toml:"operators"`
LogicEditors []string `toml:"logiceditors"`
Auditors []string `toml:"auditors"`
Admins []string `toml:"admins"`
}
type ServerConfig struct {
Listen string `toml:"listen"`
StorageDir string `toml:"storage_dir"`
MaxUpdateRateHz float64 `toml:"max_update_rate_hz"` // 0 = unlimited
// TrustedUserHeader is the HTTP header from which the end-user identity is
// read on each WebSocket connection (set by a trusted reverse proxy doing
// authentication). The identity is used for EPICS writes so each session
// can act as a different user. Empty disables it (writes use the server
// identity). MUST only be enabled when a proxy strips any client-supplied
// value, otherwise the header can be spoofed.
TrustedUserHeader string `toml:"trusted_user_header"`
// DefaultUser is the identity used when the trusted user header is absent or
// empty (e.g. unproxied/dev/LAN deployments). Empty leaves the user anonymous.
//
// Access levels (write/readonly), logic-edit, audit-view and admin rights are
// all granted via group roles (see GroupDef / [[groups]]). A config with no
// group roles at all is treated as fully open (everyone is admin), so an
// unconfigured deployment behaves like trusted LAN. Once the admin pane writes
// {storage_dir}/access.json, that file — not this config — is the source of
// truth for access.
DefaultUser string `toml:"default_user"`
// Kerberos enables native SPNEGO authentication (see KerberosConfig).
Kerberos KerberosConfig `toml:"kerberos"`
// BasicAuth enables built-in HTTP Basic authentication validated against PAM
// (see BasicAuthConfig). Mutually exclusive with Kerberos.
BasicAuth BasicAuthConfig `toml:"basic_auth"`
// LDAP enables built-in HTTP Basic authentication validated against an LDAP
// directory (see LDAPConfig). Pure-Go alternative to BasicAuth/PAM that keeps
// the static binary. Mutually exclusive with Kerberos and BasicAuth.
LDAP LDAPConfig `toml:"ldap"`
// TLS enables built-in HTTPS (see TLSConfig). Strongly recommended whenever
// BasicAuth is enabled, since Basic credentials are sent on every request.
TLS TLSConfig `toml:"tls"`
}
// KerberosConfig enables native SPNEGO/Kerberos ("Negotiate") authentication so
// uopi identifies users directly from their Kerberos ticket, without depending on
// a separate auth proxy to set TrustedUserHeader. This is the recommended setup
// for browsers like Firefox that do not silently fall back to a proxy default:
// uopi answers API requests with a 401 WWW-Authenticate: Negotiate challenge, the
// browser performs the SPNEGO handshake, and uopi resolves the user from the
// validated ticket (short principal name, realm stripped). That username feeds
// the same access pipeline as TrustedUserHeader.
//
// Browsers must be told to perform SPNEGO for this server's origin (Firefox:
// network.negotiate-auth.trusted-uris; Chrome/Edge: AuthServerAllowlist policy or
// OS integrated auth). When enabled, any inbound TrustedUserHeader value is
// ignored in favour of the Kerberos identity to prevent spoofing.
type KerberosConfig struct {
Enabled bool `toml:"enabled"`
// Keytab is the path to the service keytab holding the HTTP service
// principal's long-term key (e.g. HTTP/host.example.com@REALM). Required when
// Enabled.
Keytab string `toml:"keytab"`
// ServicePrincipal optionally selects which principal in the keytab to accept
// tickets for (e.g. "HTTP/host.example.com"). Empty accepts the keytab's
// entries by default.
ServicePrincipal string `toml:"service_principal"`
}
// BasicAuthConfig enables uopi's built-in HTTP Basic authentication: uopi
// answers API requests with 401 WWW-Authenticate: Basic, the browser prompts for
// a username/password, and uopi validates them through the host PAM stack
// (/etc/pam.d/<PAMService>). On hosts that are SSSD/LDAP clients this reuses the
// users' normal login credentials with no directory configuration in uopi. The
// validated username feeds the same access pipeline as TrustedUserHeader.
//
// PAM support requires a cgo build with the `pam` tag (make backend-pam); the
// default fully-static binary cannot validate and will refuse to start with
// BasicAuth enabled. Because Basic credentials travel on every request, enable
// TLS (see TLSConfig) unless uopi sits on a fully isolated network.
type BasicAuthConfig struct {
Enabled bool `toml:"enabled"`
// PAMService is the PAM service name under /etc/pam.d/ to authenticate
// against. Empty defaults to "uopi".
PAMService string `toml:"pam_service"`
}
// LDAPConfig enables uopi's built-in HTTP Basic authentication validated against
// an LDAP directory via the "search then bind" pattern — the same flow an
// SSSD/LDAP client uses. Because it speaks LDAP over the wire with no cgo, it
// works in the default fully-static binary (unlike the PAM backend), while still
// authenticating users against the same directory the host logs in with. The
// validated username feeds the same access pipeline as TrustedUserHeader. Enable
// TLS (ldaps:// or StartTLS) so passwords are not sent in clear text.
//
// Defaults mirror SSSD: empty UserAttr → "uid", empty UserObjectClass →
// "posixAccount", empty BindDN → anonymous search. Mutually exclusive with
// Kerberos and BasicAuth.
type LDAPConfig struct {
Enabled bool `toml:"enabled"`
// URIs are the directory endpoints (SSSD ldap_uri), tried in order, e.g.
// "ldaps://ldap.example.com". Required when Enabled.
URIs []string `toml:"uri"`
// SearchBase is the subtree user entries live under (SSSD ldap_search_base).
// Required when Enabled.
SearchBase string `toml:"search_base"`
// UserAttr is the attribute matched against the login name. Empty → "uid".
UserAttr string `toml:"user_attr"`
// UserObjectClass restricts the search. Empty → "posixAccount".
UserObjectClass string `toml:"user_object_class"`
// BindDN / BindPassword optionally authenticate the search (service account).
// Empty BindDN performs an anonymous search.
BindDN string `toml:"bind_dn"`
BindPassword string `toml:"bind_password"`
// StartTLS upgrades an ldap:// connection to TLS before binding. Ignored for
// ldaps://.
StartTLS bool `toml:"start_tls"`
// CACert is an optional PEM CA bundle to trust (private CA).
CACert string `toml:"ca_cert"`
// InsecureSkipVerify disables TLS certificate verification. Testing only.
InsecureSkipVerify bool `toml:"insecure_skip_verify"`
}
// TLSConfig enables built-in HTTPS so uopi can terminate TLS itself (e.g. for
// Basic auth) without a reverse proxy. When Enabled, Cert and Key are required.
type TLSConfig struct {
Enabled bool `toml:"enabled"`
// Cert and Key are paths to the PEM certificate and private key.
Cert string `toml:"cert"`
Key string `toml:"key"`
// RedirectFrom, when set (e.g. ":8080"), starts an additional plain-HTTP
// listener on that address that 301-redirects every request to the HTTPS
// service. Without it, a browser that connects with http:// gets the opaque
// "client sent an HTTP request to an HTTPS server" error instead of being
// upgraded. Empty disables the redirector.
RedirectFrom string `toml:"redirect_from"`
}
type DatasourceConfig struct {
Stub StubConfig `toml:"stub"`
EPICS EPICSConfig `toml:"epics"`
PVA PVAConfig `toml:"pva"`
Synthetic SyntheticConfig `toml:"synthetic"`
}
type StubConfig struct {
Enabled bool `toml:"enabled"`
}
type EPICSConfig struct {
Enabled bool `toml:"enabled"`
CAAddrList string `toml:"ca_addr_list"`
ArchiveURL string `toml:"archive_url"`
ChannelFinderURL string `toml:"channel_finder_url"`
AutoSyncFilter string `toml:"auto_sync_filter"`
AutoSyncFromArchiver bool `toml:"auto_sync_from_archiver"`
PVNames []string `toml:"pv_names"`
}
type PVAConfig struct {
Enabled bool `toml:"enabled"`
AddrList []string `toml:"addr_list"`
}
type SyntheticConfig struct {
Enabled bool `toml:"enabled"`
}
func Default() Config {
return Config{
Server: ServerConfig{
Listen: ":8080",
StorageDir: "./interfaces",
},
Datasource: DatasourceConfig{
Stub: StubConfig{Enabled: true},
EPICS: EPICSConfig{Enabled: true},
PVA: PVAConfig{Enabled: true},
Synthetic: SyntheticConfig{Enabled: true},
},
}
}
// Load reads a TOML config file and applies environment variable overrides.
// Environment variables use the prefix UOPI_ with __ as separator, e.g.
// UOPI_SERVER_LISTEN=":9090".
func Load(path string) (Config, error) {
cfg := Default()
if path != "" {
if _, err := toml.DecodeFile(path, &cfg); err != nil {
return cfg, fmt.Errorf("reading config %q: %w", path, err)
}
}
applyEnv(&cfg)
return cfg, nil
}
func applyEnv(cfg *Config) {
if v := env("UOPI_SERVER_LISTEN"); v != "" {
cfg.Server.Listen = v
}
if v := env("UOPI_SERVER_STORAGE_DIR"); v != "" {
cfg.Server.StorageDir = v
}
if v := env("UOPI_SERVER_MAX_UPDATE_RATE_HZ"); v != "" {
if hz, err := strconv.ParseFloat(v, 64); err == nil {
cfg.Server.MaxUpdateRateHz = hz
}
}
if v := env("UOPI_SERVER_TRUSTED_USER_HEADER"); v != "" {
cfg.Server.TrustedUserHeader = v
}
if v := env("UOPI_SERVER_DEFAULT_USER"); v != "" {
cfg.Server.DefaultUser = v
}
if v := env("UOPI_SERVER_KERBEROS_ENABLED"); v != "" {
cfg.Server.Kerberos.Enabled = (v == "true" || v == "YES")
}
if v := env("UOPI_SERVER_KERBEROS_KEYTAB"); v != "" {
cfg.Server.Kerberos.Keytab = v
}
if v := env("UOPI_SERVER_KERBEROS_SERVICE_PRINCIPAL"); v != "" {
cfg.Server.Kerberos.ServicePrincipal = v
}
if v := env("UOPI_SERVER_BASIC_AUTH_ENABLED"); v != "" {
cfg.Server.BasicAuth.Enabled = (v == "true" || v == "YES")
}
if v := env("UOPI_SERVER_BASIC_AUTH_PAM_SERVICE"); v != "" {
cfg.Server.BasicAuth.PAMService = v
}
if v := env("UOPI_SERVER_TLS_ENABLED"); v != "" {
cfg.Server.TLS.Enabled = (v == "true" || v == "YES")
}
if v := env("UOPI_SERVER_TLS_CERT"); v != "" {
cfg.Server.TLS.Cert = v
}
if v := env("UOPI_SERVER_TLS_KEY"); v != "" {
cfg.Server.TLS.Key = v
}
if v := env("UOPI_SERVER_LDAP_ENABLED"); v != "" {
cfg.Server.LDAP.Enabled = (v == "true" || v == "YES")
}
if v := env("UOPI_SERVER_LDAP_URI"); v != "" {
cfg.Server.LDAP.URIs = strings.Fields(v)
}
if v := env("UOPI_SERVER_LDAP_SEARCH_BASE"); v != "" {
cfg.Server.LDAP.SearchBase = v
}
if v := env("UOPI_SERVER_LDAP_BIND_DN"); v != "" {
cfg.Server.LDAP.BindDN = v
}
if v := env("UOPI_SERVER_LDAP_BIND_PASSWORD"); v != "" {
cfg.Server.LDAP.BindPassword = v
}
if v := env("UOPI_AUDIT_ENABLED"); v != "" {
cfg.Audit.Enabled = (v == "true" || v == "YES")
}
if v := env("UOPI_AUDIT_DB_PATH"); v != "" {
cfg.Audit.DBPath = v
}
if v := env("UOPI_EPICS_CA_ADDR_LIST"); v != "" {
cfg.Datasource.EPICS.CAAddrList = v
}
if v := env("UOPI_EPICS_ARCHIVE_URL"); v != "" {
cfg.Datasource.EPICS.ArchiveURL = v
}
if v := env("UOPI_EPICS_CHANNEL_FINDER_URL"); v != "" {
cfg.Datasource.EPICS.ChannelFinderURL = v
}
if v := env("UOPI_EPICS_AUTO_SYNC_FILTER"); v != "" {
cfg.Datasource.EPICS.AutoSyncFilter = v
}
if v := env("UOPI_EPICS_AUTO_SYNC_FROM_ARCHIVER"); v != "" {
cfg.Datasource.EPICS.AutoSyncFromArchiver = (v == "true" || v == "YES")
}
if v := env("EPICS_PVA_ADDR_LIST"); v != "" {
cfg.Datasource.PVA.AddrList = strings.Fields(v)
}
if v := env("UOPI_UI_DEFAULT_ZOOM"); v != "" {
if z, err := strconv.ParseFloat(v, 64); err == nil {
cfg.UI.DefaultZoom = z
}
}
}
func env(key string) string {
return strings.TrimSpace(os.Getenv(key))
}