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
+97 -1
View File
@@ -384,7 +384,39 @@ instance's `setId` in its `Meta`.
Identity and global policy live in `internal/access` (`Policy`, `Role`, `Level`). The user
identity is read per request from `server.trusted_user_header` (with a `default_user`
fallback) and stored on the request context. Access is **role-based** through group
fallback) and stored on the request context. Alternatively, **native SPNEGO/Kerberos**
authentication (`[server.kerberos]`) lets uopi identify users directly from their Kerberos
ticket without a reverse proxy: `internal/server/kerberos.go` wraps the REST and WebSocket
handlers, challenges API requests with `401 WWW-Authenticate: Negotiate`, validates the
ticket against the configured service keytab (`github.com/jcmturner/gokrb5`), and writes the
short principal name (realm stripped) into the same `userHeader` the access pipeline reads
— so downstream identity resolution is identical. Any client-supplied header value is
discarded before validation to prevent spoofing. WebSocket upgrades (where browsers cannot
attach an `Authorization` header) validate proactively-sent credentials best-effort and
otherwise fall back to `default_user`. Browsers must be configured to perform SPNEGO for the
server origin (Firefox: `network.negotiate-auth.trusted-uris`), which fixes the case where
Firefox would otherwise resolve to `default_user`. A third standalone option is **built-in
HTTP Basic authentication** (`[server.basic_auth]`, mutually exclusive with Kerberos):
`internal/server/basicauth.go` challenges with `401 WWW-Authenticate: Basic` and validates
credentials against the host PAM stack (`internal/pamauth`, `/etc/pam.d/<pam_service>`), so
on an SSSD/LDAP-joined host users authenticate with their normal login password and no
directory schema is needed in uopi. Successful logins are memoised in a short-TTL salted-hash
cache (`credCache`) to avoid a PAM round-trip per request, and the validated username is
written into the same `userHeader`. PAM requires a cgo build (`make backend-pam`, build tag
`pam`); the default static binary uses a stub that refuses Basic auth. As a **pure-Go**
alternative that keeps the fully-static (`CGO_ENABLED=0`) binary, `[server.ldap]`
(`internal/ldapauth`) validates the same Basic credentials against an LDAP directory with a
"search then bind" — anonymously (or via a service `bind_dn`) locate the user entry under
`search_base`, then bind as its DN with the supplied password — mirroring an SSSD/LDAP
client (defaults: `user_attr=uid`, `user_object_class=posixAccount`). Empty passwords are
rejected before any bind to avoid LDAP "unauthenticated bind", and the login name is
filter-escaped against injection. The challenge front-end is shared: both PAM and LDAP feed
the same `basicAuth` middleware, and the page load (`/`) is challenged too so the browser's
native login dialog actually appears (a background `fetch('/me')` 401 does not prompt).
Because Basic credentials are sent on every request, uopi can terminate **TLS** itself
(`[server.tls]``ListenAndServeTLS`) without a reverse proxy. The four identity sources
(proxy header, Kerberos, PAM-Basic, LDAP-Basic) all resolve to the same `userHeader` and are
mutually exclusive where they overlap. Access is **role-based** through group
memberships: each `[[groups]]` block lists members by role along the cumulative ladder
`viewer < operator < logiceditor < auditor < admin`, and a user's **effective** global
capability is the highest role across all their memberships. Roles map to capabilities as:
@@ -425,6 +457,39 @@ opened from the view-mode Tools dropdown when `canAdmin`) presents Users (per-gr
assignment with an effective-role badge), Groups (nesting + per-member roles), and
Server-stats tabs.
#### Visibility scope (selector-tree filtering)
Every user-owned, list-able object — panels, synthetic signals, config sets/instances, and
control-logic graphs — carries a uniform **visibility scope** so each selector tree can be
filtered by **Mine / Group / Global**. The model is `owner` (stamped server-side from the
trusted identity on create, immutable across updates) plus a scope token
`∈ {private, group, global}` and, for group scope, a list of group names. An empty or
unknown token resolves to **global**, so legacy objects with no scope stay visible to
everyone. This is a *visibility filter*, not a hard security boundary: an owner always sees
their own objects regardless of scope, and the per-panel ACL (§3.10) remains the real
access-control mechanism for panels.
The shared backend helper is `access.CanSee(user, owner, scope, itemGroups, userGroups)`
(`internal/access/scope.go`), with a `(*Policy).CanSee` method that resolves the caller's
groups via `GroupsOf`. Each list endpoint filters its results through it:
`internal/confmgr` stores `owner/scope/groups` on sets and instances (filtered by
`filterConfigMetas`); synthetic `SignalDef` gained a `group` visibility mode routed through
`synVisible`; control-logic `Graph` carries `owner/scope/scopeGroups` (named to avoid the
pre-existing cosmetic `Groups []NodeGroup`) filtered in `listControlLogic`. Panels reuse the
existing ACL rather than a new field: `panelScope` (`internal/api/api.go`) derives the
bucket from the ACL record (public → global, a group grant → group, otherwise → private;
unmanaged → global) and surfaces it on `InterfaceListItem.scope`/`groups`.
The shared frontend lib is `web/src/lib/scope.tsx`: `bucketOf` assigns an item to exactly
one bucket (owned → mine, else group-scoped → group, else global), `filterByScope` narrows a
list to the active bucket (with an optional groups-accessor for control-logic's
`scopeGroups`), `ScopeFilter` is the segmented `[Mine | Group ▾ | Global]` selector shown
above each tree (the Group segment carries a combo to pick a group when the user is in
several), and `ScopePicker` is the create/save visibility editor. `ConfigManager.tsx`,
`SyntheticGraphEditor.tsx`, and `ControlLogicEditor.tsx` use the picker to set scope on save;
panel visibility is instead edited through the existing Share dialog. `InterfaceList.tsx`
applies the filter to its folder tree, hiding folders that have no in-scope descendant.
### 3.11 Configuration Manager
The configuration manager is `internal/confmgr`: a two-tier model of **sets** (typed
@@ -534,6 +599,7 @@ Both edit and view modes support mouse-drag panel resizing:
- `html { font-size: clamp(13px, 1.5vh, 18px); }` — base font scales with viewport height, making the UI naturally larger on 4K screens where the browser zoom level is 100%.
- Key structural heights (toolbar, panel headers, tab bar, plot toolbar) are expressed in `rem` so they scale with the base font.
- **ZoomControl** (A / % / A+) in the toolbar lets users manually override the zoom level in 11 steps from 50% to 250%. The preference is persisted in `localStorage` (`uopi:ui-zoom`) and applied by setting `document.documentElement.style.fontSize` on load.
- The root font-size also folds in `window.devicePixelRatio` (`applyZoom``16 × zoom × dpr`) so high-DPI displays auto-scale the UI by default; Firefox in particular does not enlarge the root px on its own, so the DPR must be applied explicitly. `watchDpr()` re-applies the zoom when the ratio changes (e.g. the window moves to a differently-scaled monitor).
- Canvas pixel rendering (uPlot, ECharts) reads `window.devicePixelRatio` and sizes canvases accordingly.
### 4.8 Lua Editor (`LuaEditor.tsx`)
@@ -622,6 +688,36 @@ storage_dir = "./interfaces"
trusted_user_header = "" # header carrying the proxy-authenticated user
default_user = "" # identity when the header is absent (LAN/dev)
# Native SPNEGO/Kerberos auth — alternative to a proxy; identifies users from
# their Kerberos ticket. Recommended when some browsers (e.g. Firefox) would
# otherwise fall through to default_user.
# [server.kerberos]
# enabled = true
# keytab = "/etc/uopi/http.keytab" # HTTP/host@REALM service key
# service_principal = "HTTP/host.example.com" # optional; empty = keytab default
# Built-in HTTP Basic auth (PAM) — standalone, mutually exclusive with Kerberos.
# Requires a PAM build: `make backend-pam`. Enable TLS below for production.
# [server.basic_auth]
# enabled = true
# pam_service = "uopi" # /etc/pam.d/<name>; empty = "uopi"
# Built-in HTTP Basic auth (LDAP) — pure-Go, works in the static binary; search
# then bind against the directory. Mutually exclusive with kerberos/basic_auth.
# [server.ldap]
# enabled = true
# uri = ["ldaps://ldap.example.com"]
# search_base = "dc=example,dc=com"
# user_attr = "uid" # "sAMAccountName" for AD; empty = uid
# bind_dn = "" # empty = anonymous search
# Built-in TLS/HTTPS — terminate HTTPS without a reverse proxy. Recommended
# whenever basic_auth is enabled. Both cert and key required when enabled.
# [server.tls]
# enabled = true
# cert = "/etc/uopi/tls/cert.pem"
# key = "/etc/uopi/tls/key.pem"
# Role-based access through group memberships. Roles (low→high):
# viewer < operator < logiceditor < auditor < admin
# Effective capability = highest role across all memberships. The built-in