Initial commit: scadaproj umbrella — sister-project index, auth component normalization (design + GAPS), and the built ZB.MOM.WW.Auth shared library (0.1.0, flattened in).
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# Canonical roles (standardized)
|
||||
|
||||
Status: **Standardized**. The org-wide role set every sister project maps onto. This is a
|
||||
mapping **layer above** each project's native authorization: native *enforcement* (OtOpcUa
|
||||
`NodePermissions`, mxaccessgw gRPC scopes, ScadaBridge native roles + site-scoping) is
|
||||
unchanged — each project adds a `canonical → native` expansion via the `IGroupRoleMapper<CanonicalRole>`
|
||||
seam ([`SPEC.md`](SPEC.md) §3). LDAP groups (or API keys) are assigned a **canonical** role;
|
||||
the project translates it to native permissions at runtime.
|
||||
|
||||
## Capabilities
|
||||
|
||||
| Capability | Meaning |
|
||||
|---|---|
|
||||
| `OBSERVE` | browse / read / subscribe / history **+ read audit logs** |
|
||||
| `OPERATE` | operate-level writes; alarm acknowledge / confirm |
|
||||
| `TUNE` | tune-level writes; alarm shelve; method calls |
|
||||
| `AUTHOR` | create / edit configuration & templates |
|
||||
| `DEPLOY` | publish / push configuration to runtime / sites (scoped) |
|
||||
| `ADMINISTER` | manage users / security / system **+ export audit** |
|
||||
|
||||
There is no standalone `AUDIT` capability and **no Auditor role**: audit *read* is part of
|
||||
`OBSERVE`, audit *export* is part of `ADMINISTER`.
|
||||
|
||||
## The six roles (capability bundles)
|
||||
|
||||
| Role | OBSERVE | OPERATE | TUNE | AUTHOR | DEPLOY | ADMINISTER |
|
||||
|---|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| **Viewer** | ✓ | | | | | |
|
||||
| **Operator** | ✓ | ✓ | | | | |
|
||||
| **Engineer** | ✓ | ✓ | ✓ | | | |
|
||||
| **Designer** | ✓ | | | ✓ | | |
|
||||
| **Deployer** | ✓ | | | | ✓ | |
|
||||
| **Administrator** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
Two privilege axes meet at the top: **operations** (Viewer → Operator → Engineer) and
|
||||
**configuration lifecycle** (Viewer → Designer → Deployer → Administrator). Administrator is
|
||||
the only role that holds every capability.
|
||||
|
||||
## Orthogonal modifiers (not roles)
|
||||
|
||||
- **Scope** — a grant is `role × scope`. OtOpcUa scopes by OPC-UA node tree
|
||||
(cluster→area→line→equipment→tag); ScadaBridge scopes `Deployer` by **site**; mxaccessgw
|
||||
scopes API keys by subtree/tag globs. The canonical layer carries an abstract scope selector;
|
||||
each project resolves it to its own granularity.
|
||||
- **Principal type** — humans (LDAP group → canonical role) and machines (API key → a subset of
|
||||
canonical capabilities) both take canonical roles.
|
||||
|
||||
## Mapping to each project
|
||||
|
||||
| Canonical | OtOpcUa | MxAccessGateway | ScadaBridge |
|
||||
|---|---|---|---|
|
||||
| **Viewer** | data-plane `ReadOnly` bundle; ctrl `ConfigViewer` | dashboard `Viewer`; scopes `invoke:read`+`metadata:read`+`events:read`+`session:*` | `AuditReadOnly` (← audit read); ⚠ no general config-viewer role yet |
|
||||
| **Operator** | data-plane `Operator` bundle | scope `invoke:write` | ⚠ N/A — runtime ops live at sites, not a central role |
|
||||
| **Engineer** | data-plane `Engineer` bundle | scope `invoke:secure` (closest) | ⚠ N/A |
|
||||
| **Designer** | ctrl `ConfigEditor` (+ `WriteConfigure`) | ⚠ N/A — no config-authoring surface | `Design` |
|
||||
| **Deployer** | ◑ part of `FleetAdmin` (config publish/generation) | ⚠ N/A | `Deployment` (+ site scope) |
|
||||
| **Administrator** | ctrl `FleetAdmin`; data-plane `Admin` bundle | dashboard `Admin`; scope `admin` | `Admin` (+ `Audit` export ←) |
|
||||
|
||||
## Consequences & gaps (accepted)
|
||||
|
||||
- **Auditor removed:** ScadaBridge `AuditReadOnly` → **Viewer**, `Audit` → **Administrator**.
|
||||
This **loses ScadaBridge's auditor/admin separation-of-duties** (an auditor who can export but
|
||||
is not a full admin no longer has a distinct role). Accepted as the cost of standardizing.
|
||||
- Each project implements only the **subset** of canonical roles that applies; the ⚠ cells are
|
||||
simply never assigned there (ScadaBridge: no Operator/Engineer; mxaccessgw: no Designer/Deployer).
|
||||
- OtOpcUa has no first-class Deployer (config publish ⊂ `FleetAdmin`) — `Deployer` maps partially
|
||||
until/unless OtOpcUa splits a publish-only control-plane role.
|
||||
- Adoption is governance + a mapping layer, not enforcement changes — see [`GAPS.md`](../GAPS.md) backlog.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Auth — normalized target spec
|
||||
|
||||
Status: **Draft**. The single design the sister projects converge on. Derived from the
|
||||
three code-verified current-state docs (`../current-state/`). Goal is *path to shared code*
|
||||
(`../shared-contract/ZB.MOM.WW.Auth.md`), so each normalized section maps to a shared library seam.
|
||||
|
||||
## 0. Scope
|
||||
|
||||
**Normalized here:** LDAP/identity config schema; bind-then-search behavior; the
|
||||
group→role mapping *seam*; **the standardized canonical role set every project maps onto
|
||||
([`CANONICAL-ROLES.md`](CANONICAL-ROLES.md))**; the API-key contract; cookie/claim conventions;
|
||||
dev conventions; secret handling.
|
||||
|
||||
**Explicitly NOT normalized** (domain-specific — keep per project): the authorization
|
||||
*enforcement* (OtOpcUa `NodePermissions` ACL trie, mxaccessgw gRPC scopes + constraints,
|
||||
ScadaBridge native roles + site-scoping) — each project **maps its native model onto the
|
||||
canonical roles** but keeps its own enforcement; OPC UA transport security; OtOpcUa's
|
||||
generation/staleness session model; ScadaBridge's site-scope rules; mxaccessgw's hub-token model.
|
||||
|
||||
## 1. LDAP / identity configuration schema
|
||||
|
||||
One **nested `Ldap` options object**, bound under each app's own root section
|
||||
(`OtOpcUa:Authentication:Ldap`, `MxGateway:Ldap`, `ScadaBridge:Security:Ldap`). Canonical keys/types:
|
||||
|
||||
| Key | Type | Notes |
|
||||
|---|---|---|
|
||||
| `Enabled` | bool | |
|
||||
| `Server` | string | host |
|
||||
| `Port` | int | 389 / 636 / 3893 (GLAuth dev) |
|
||||
| `Transport` | enum `Ldaps` \| `StartTls` \| `None` | **adopt ScadaBridge's enum** over a `UseTls` bool — it expresses StartTLS, which the bool can't |
|
||||
| `AllowInsecure` | bool (default false) | dev-only escape hatch; pairs with `Transport=None` |
|
||||
| `SearchBase` | string | base DN |
|
||||
| `ServiceAccountDn` | string | for bind-then-search |
|
||||
| `ServiceAccountPassword` | string | from secret store, never source |
|
||||
| `UserNameAttribute` | string | canonical name (default `cn` GLAuth / `sAMAccountName` AD). Supersedes ScadaBridge's `LdapUserIdAttribute` |
|
||||
| `DisplayNameAttribute` | string | default `cn` |
|
||||
| `GroupAttribute` | string | default `memberOf` |
|
||||
| `ConnectionTimeoutMs` | int | per-operation socket timeout |
|
||||
|
||||
Migration: OtOpcUa `Authentication.Ldap.UseTls`→`Transport`; ScadaBridge flat `Ldap*`→nested + rename `LdapUserIdAttribute`→`UserNameAttribute`; gateway `MxGateway:Ldap` already close.
|
||||
|
||||
## 2. Bind-then-search behavior (canonical algorithm)
|
||||
|
||||
1. Connect to `Server:Port`. If `Transport != Ldaps/StartTls` and not `AllowInsecure` → **refuse** (config error).
|
||||
2. Bind the **service account**; a failure here is a *system misconfiguration*, surfaced distinctly from bad user creds.
|
||||
3. Search under `SearchBase` with filter `({UserNameAttribute}={escapedUsername})`. **Escape** the username (LDAP filter rules: `\ * ( ) NUL`). Reject **multiple** matches (ambiguous); no match → auth failure.
|
||||
4. **Re-bind as the resolved user DN** with the supplied password to verify it. RFC 4514-escape DN components.
|
||||
5. Read `GroupAttribute`; normalize group names (strip leading `CN=`/RDN). **If the group lookup fails, fail the login** — never admit a user with zero resolved groups.
|
||||
6. **Trim-normalize** the username once at entry so one person ≠ two identities.
|
||||
|
||||
Generic, injection-safe, fail-closed. (ScadaBridge's current impl is the closest reference.)
|
||||
|
||||
## 3. Group → role mapping seam
|
||||
|
||||
Resolved LDAP groups (and API-key principals) map to the **standardized canonical role set**
|
||||
([`CANONICAL-ROLES.md`](CANONICAL-ROLES.md)) via `IGroupRoleMapper<CanonicalRole>`; each project
|
||||
then **expands a canonical role into its native permissions/scopes** and applies its own scope
|
||||
payload. The backing store is project-chosen — **config dict** (OtOpcUa `GroupToRole`, gateway
|
||||
`Dashboard:GroupToRole`) **or database** (ScadaBridge `LdapGroupMapping`). The shared library
|
||||
defines the seam + the `CanonicalRole` enum and ships both a config-backed and a delegate/DB-backed
|
||||
mapper; native enforcement stays per-project.
|
||||
|
||||
## 4. API-key contract (machine-to-machine)
|
||||
|
||||
For projects with a programmatic surface (mxaccessgw gRPC, ScadaBridge Inbound API):
|
||||
|
||||
- **Token:** `<prefix>_<keyId>_<secret>` (prefix configurable, e.g. `mxgw`). Parsed/validated before any store hit.
|
||||
- **Hashing:** HMAC-SHA256 with an **external pepper** (from secret store/config, never stored beside the hash). Secret = 32 random bytes, URL-safe base64.
|
||||
- **Verify:** lookup by keyId → reject if revoked → hash presented secret with pepper → **constant-time compare** (`CryptographicOperations.FixedTimeEquals`). Discriminated failure reasons for audit; opaque error to the caller.
|
||||
- **Store:** abstraction with a default SQLite implementation (hash, scopes, optional constraints, created/last-used/revoked) + **append-only audit**. Revoke = timestamp; delete only when revoked.
|
||||
- **Scopes:** a `string` set gating operations (project supplies the catalog).
|
||||
- **Constraints:** an **opaque, project-supplied policy** object (mxaccessgw subtree/tag globs + `MaxWriteClassification`; ScadaBridge per-method approval). The contract carries it; it does not hard-code either shape.
|
||||
- **Admin CLI:** `init-db / create-key / list-keys / revoke-key / rotate-key / delete-key`.
|
||||
|
||||
(mxaccessgw Model A is the reference implementation to extract.)
|
||||
|
||||
## 5. Cookie / claim / session conventions
|
||||
|
||||
- **Cookie:** HttpOnly, `SameSite=Strict`, `Secure` configurable for dev (`RequireHttpsCookie`), sliding idle expiry. Name pattern `<App>.Auth`.
|
||||
- **Claims:** canonical claim types for name, display name, username, role (one per role), and any project scope id — defined once in `ZB.MOM.WW.Auth.AspNetCore`.
|
||||
- **DataProtection** keys persisted to shared storage so cookies/tokens survive node failover (OtOpcUa already does this via Config DB).
|
||||
- Session *lifetime policy* (refresh windows, staleness, generation binding) stays per-project.
|
||||
|
||||
## 6. Dev & secret conventions
|
||||
|
||||
- **Dev directory:** GLAuth. **Unify the dev base DN** — today OtOpcUa/gateway use `dc=lmxopcua,dc=local` and ScadaBridge uses `dc=scadabridge,dc=local`; pick one shared dev base DN (see `../GAPS.md`).
|
||||
- **Dev escape hatches** named consistently: `AllowInsecure` (LDAP), plus each project's documented bypass (`AllowAnonymousLocalhost`, `DevStubMode`) clearly dev-only and off in prod.
|
||||
- **Secrets:** service-account passwords, JWT signing keys, and API-key peppers come from a secret store / env, never source; never logged (API keys, passwords, secured payloads, credentials).
|
||||
|
||||
## 7. Acceptance (what "converged" means)
|
||||
|
||||
A project is converged when: (a) its LDAP authn + (if applicable) API-key auth run on the
|
||||
`ZB.MOM.WW.Auth` packages; (b) its config matches §1's schema; (c) its group→role mapping
|
||||
implements the §3 seam; (d) cookie/claim conventions match §5; with all project-specific
|
||||
authorization unchanged.
|
||||
Reference in New Issue
Block a user