Added ldap and pam authentication
This commit is contained in:
+97
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user