Added ldap and pam authentication

This commit is contained in:
Martino Ferrari
2026-06-23 17:59:40 +02:00
parent ac24011487
commit 11120bedca
36 changed files with 1935 additions and 66 deletions
+166
View File
@@ -13,10 +13,22 @@ 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
@@ -65,6 +77,116 @@ type ServerConfig struct {
// {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 {
@@ -146,6 +268,45 @@ func applyEnv(cfg *Config) {
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")
}
@@ -170,6 +331,11 @@ func applyEnv(cfg *Config) {
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 {