# Component: Security & Auth

## Purpose

The Security & Auth component handles user authentication via LDAP/Active Directory and enforces role-based authorization across the system. It maps LDAP group memberships to system roles and applies permission checks to all operations.

## Location

Central cluster. Sites do not have user-facing interfaces and do not perform independent authentication.

## Responsibilities

- Authenticate users against LDAP/Active Directory using a direct LDAP/AD bind (username/password).
- Map LDAP group memberships to system roles.
- Enforce role-based access control on all API and UI operations.
- Support site-scoped permissions for the Deployer role.

## Authentication

- **Mechanism**: The Central UI presents a username/password login form. The app validates credentials by binding to the LDAP/AD server with the provided credentials, then queries the user's group memberships.
- **Transport security**: LDAP connections **must** use LDAPS (port 636) or StartTLS to encrypt credentials in transit. Unencrypted LDAP (port 389) is not permitted.
- **No local user store**: All identity and group information comes from AD. No credentials are cached locally.
- **No Windows Integrated Authentication**: The app authenticates directly against LDAP/AD, not via Kerberos/NTLM.

## Dev Disable-Login Flag

**`ScadaBridge:Security:Auth:DisableLogin`** (bool, default `false`) — when `true`, the Central UI bypasses the login form entirely and auto-authenticates every request as the user named by `ScadaBridge:Security:Auth:User` (default `multi-role`) with all six roles (Administrator, Designer, Deployer, Viewer, Operator, Verifier) granted system-wide. The mechanism is `AutoLoginAuthenticationHandler`, registered under the cookie scheme via `AddSecurity(disableLogin: true)`; because it sits in the cookie scheme, every existing authorization policy authenticates through it with zero policy changes required.

**Environment guard (`DisableLoginGuard`).** Because the flag also grants Operator+Verifier — nullifying the two-person secured-write control — `DisableLogin=true` is **refused outside the Development environment**. `DisableLoginGuard.Validate` runs at the Host composition root (before `AddSecurity`) and throws at startup unless the environment is `Development` (case-insensitive) or the operator sets the explicit second acknowledgement key **`ScadaBridge:Security:Auth:AllowOutsideDevelopment`** (bool, default `false`). The ack key exists only for the rare deliberate case of an operator who fully accepts an unauthenticated deployment; absent it, a non-Development host fails fast rather than booting unauthenticated.

> **Dev/test ONLY. Never enable in production.**

Set in a local or docker-dev environment via the environment variable `ScadaBridge__Security__Auth__DisableLogin=true`. Note that `ScadaBridge:Security:Auth` is a child sub-section nested inside `ScadaBridge:Security`.

## Session Management

### Cookie + JWT Hybrid
- On successful authentication, the app issues a **JWT** signed with a shared symmetric key (HMAC-SHA256). Both central cluster nodes use the same signing key from configuration, so either node can issue and validate tokens.
- The JWT is embedded in an **authentication cookie** rather than being passed as a bearer token. This is the correct transport for Blazor Server, where persistent SignalR circuits do not carry Authorization headers — the browser automatically sends the cookie with every SignalR connection and HTTP request.
- The cookie is **HttpOnly** and **Secure** (requires HTTPS).
- On each request, the server extracts and validates the JWT from the cookie. All authorization decisions are made from the JWT claims without hitting the database.
- **JWT claims**: User display name, username, list of roles (`Administrator`, `Designer`, `Deployer`, `Viewer`), and for site-scoped Deployer, the list of permitted site IDs.

### Token Lifecycle

> **Implementation note (M2.19, #15).** The interactive Central UI login path signs in
> with **bare cookie claims**, not a cookie-embedded JWT. The session lifecycle below is
> therefore enforced by the cookie middleware (`ExpireTimeSpan` + `SlidingExpiration`) plus
> a `CookieAuthenticationEvents.OnValidatePrincipal` handler — see **Session Validation
> (`OnValidatePrincipal`)** below. The embedded-JWT model remains the documented design
> intent and is the mechanism for any non-cookie bearer surface (e.g. `/auth/token`), but
> it is **not** the transport for the cookie principal.

- **Idle timeout**: Configurable, default **30 minutes**. If no requests are made within the idle window, the session is rejected and the user must re-login. Tracked via a `LastActivity` last-activity timestamp claim. The cookie's `ExpireTimeSpan` is set to the idle timeout and `SlidingExpiration` renews it on activity, so the cookie window and the explicit `OnValidatePrincipal` idle check use the **same** value and cannot contradict each other.
- **Role-mapping refresh (LDAP-free)**: Configurable, default **15 minutes** (`SecurityOptions.RoleRefreshThresholdMinutes`). At login the session stores the user's raw LDAP groups (one `zb:group` claim each) plus a `zb:lastrolerefresh` anchor. Once the anchor is older than the threshold, `OnValidatePrincipal` re-runs the **DB-backed** `RoleMapper` on the stored groups — **with no LDAP call** — rebuilds the role/scope claims via the shared claim-builder, advances the anchor, and re-issues the cookie. Central role-mapping (DB) changes — including a **revoked** mapping that drops the user's roles, and changed site-scope rules — take effect within this window. Roles derived from central mappings are never more than ~15 minutes stale.

#### Session Validation (`OnValidatePrincipal`)
- The cookie principal is built at login by a **single shared claim-builder** (`SessionClaimBuilder`). The `OnValidatePrincipal` role-refresh path rebuilds the principal through the **same** builder, so the login and refresh claim shapes cannot drift.
- **Failure policy**: the refresh is best-effort. Any error during the refresh (e.g. the configuration database is unreachable) **keeps the existing principal with its current roles** — it never signs the user out and never throws out of the request pipeline. This mirrors the **Active sessions** stance under *LDAP Connection Failure* below. Only the explicit idle-timeout path rejects the principal.

> **Residual limitation — live LDAP group-membership changes (follow-up).** The
> mid-session refresh re-maps the **stored** groups against the central database; it does
> **not** re-query LDAP, so a change to the user's actual **group membership** in the
> directory is picked up only at **next login**. A live group re-query for an active
> session would require a new passwordless service-account group-search method on the
> shared `ZB.MOM.WW.Auth.Ldap` library, which is an **external NuGet package** and exposes
> only `AuthenticateAsync(username, password, ct)` (no standalone group search). Adding
> that method is tracked as a follow-up. Until then: central role-mapping/scope changes are
> reflected within ~15 minutes; directory group-membership changes require re-login.

### Load Balancer Compatibility
- The authentication cookie carries a self-contained JWT — no server-side session state. A load balancer in front of the central cluster can route requests to either node without sticky sessions or a shared session store.
- Since both central nodes share the same JWT signing key, either node can validate the cookie-embedded JWT. Central failover is transparent to users with valid cookies.

## LDAP Connection Failure

- **New logins**: If the LDAP/AD server is unreachable, login attempts **fail**. Users cannot be authenticated without LDAP.
- **Active sessions**: Users with a valid (not-idle-timed-out) session can **continue operating** with their current roles during an LDAP outage. Interactive cookie sessions never re-query LDAP mid-session (the mid-session role-mapping refresh is DB-only — see *Session Validation* above), so a brief LDAP outage does not disrupt engineers mid-work; central role-mapping changes still apply within the refresh window regardless of LDAP availability.
- **Recovery (group-membership changes)**: Because the mid-session refresh is LDAP-free, a change to a user's **directory group membership** is picked up at the user's **next login** (when LDAP is queried again), not mid-session — see the *Residual limitation* note above.

## Roles

### Administrator
- **Scope**: System-wide (always).
- **Permissions**:
  - Manage site definitions.
  - Manage site-level data connections (define and assign to sites).
  - Manage area definitions per site.
  - Manage LDAP group-to-role mappings.
  - Manage API keys (create, enable/disable, delete).
  - System-level configuration.
  - View and export audit logs.

### Designer
- **Scope**: System-wide (always).
- **Permissions**:
  - Create, edit, delete templates (including attributes, alarms, scripts).
  - Manage shared scripts.
  - Manage external system definitions.
  - Manage database connection definitions.
  - Manage notification lists and SMTP configuration.
  - Manage inbound API method definitions.
  - Run on-demand validation (template flattening, script compilation).

### Deployer
- **Scope**: System-wide or site-scoped.
- **Permissions**:
  - Create and manage instances (overrides, connection bindings, area assignment).
  - Disable, enable, and delete instances.
  - Deploy configurations to instances.
  - Deploy system-wide artifacts (shared scripts, external system definitions, DB connections, data connections) to all sites.
  - View deployment diffs and status.
  - Use debug view.
  - Manage parked messages.
  - Monitor and manage the Notification Outbox (retry and discard parked notifications).
  - View site event logs.
- **Site scoping**: A user with site-scoped Deployer role can only perform these actions for instances at their permitted sites.

### Viewer
- **Scope**: System-wide (always).
- **Permissions**:
  - Read-only access to operational audit data (Audit Log, Notifications, Site Calls).
  - Navigation to audit pages.
  - Does not grant export or any mutating action.

### Operator
- **Scope**: System-wide (always).
- **Permissions**:
  - Submit a **secured write** to an MxAccess Gateway connection from the Central UI Secured Writes page (`/operations/secured-writes`) — the *initiating* half of the two-person write workflow. The **Tag path** is selected via the shared `NodeBrowserDialog` tag browser ("Browse…" button; the dialog's address-space search box is hidden for MxGateway connections, which have no search capability). The **Data type** is auto-filled best-effort from the selected tag's Galaxy data type via `SecuredWriteDataTypeMapper` and remains operator-editable.
- **Purpose**: One of the two distinct global roles backing MxGateway secured writes. An Operator initiates a write; it never executes until a separate **Verifier** approves it. Holding Operator alone never executes a write.

### Verifier
- **Scope**: System-wide (always).
- **Permissions**:
  - Approve or reject a pending secured write from the Secured Writes page — the *approving* half of the two-person write workflow.
- **Purpose**: The approving counterpart to **Operator**. Separation of duties is enforced **server-side**: the ManagementActor rejects any approval where the approving user equals the submitting Operator (no self-approval), so the two roles must be held by distinct principals for a write to execute. (See Component-ManagementService.md and Component-CentralUI.md.)

> **Two-person secured-write workflow.** `Operator` and `Verifier` are deliberately separate global roles so a single principal cannot both initiate and approve a write through the MxAccess Gateway. Both are coarse global roles like the others; any site scoping is layered on at the LDAP-mapping level. Note the dev `DisableLogin` caveat: with `DisableLogin` on, the auto-login principal holds **all** roles, so the two-person flow cannot be exercised end-to-end by a single identity — no-self-approval is covered by handler tests and real two-person use requires two real identities.

## Authorization Policies

Role checks are expressed as named ASP.NET Core authorization policies (in `AuthorizationPolicies`), each requiring the matching role claim:

| Policy | Role required | Used by |
|--------|---------------|---------|
| `RequireAdmin` | Administrator | Site/data-connection management, API keys, LDAP mappings, bundle import, server-certificate trust/remove |
| `RequireDesign` | Designer | Template authoring, shared scripts, external/DB definitions, notification lists, bundle export |
| `RequireDeployment` | Deployer | Instance/deployment management, debug view, parked messages, Notification Outbox, Alarm Summary |
| `RequireOperator` | Operator | Submit a secured write (Secured Writes page) |
| `RequireVerifier` | Verifier | Approve/reject a pending secured write |

`RequireOperator` and `RequireVerifier` were added with the two-person secured-write feature (M7); the rest predate it.

## Multi-Role Support

- A user can hold **multiple roles simultaneously** by being a member of multiple LDAP groups.
- Roles are **independent** — there is no implied hierarchy between roles.
- For example, a user who is a member of both `SCADA-Designers` and `SCADA-Deploy-All` holds both the Designer and Deployer roles, allowing them to author templates and also deploy configurations.

## LDAP Group Mapping

- System administrators configure mappings between LDAP groups and roles.
- Examples:
  - `SCADA-Admins` → Administrator role
  - `SCADA-Designers` → Designer role
  - `SCADA-Deploy-All` → Deployer role (all sites)
  - `SCADA-Deploy-SiteA` → Deployer role (Site A only)
  - `SCADA-Deploy-SiteB` → Deployer role (Site B only)
  - `SCADA-Operators` → Operator role (initiates secured writes)
  - `SCADA-Verifiers` → Verifier role (approves secured writes)
- A user can be a member of multiple groups, granting multiple independent roles.
- Group mappings are stored in the configuration database and managed via the Central UI (Administrator role).

## Permission Enforcement

- Every API endpoint and UI action checks the authenticated user's roles before proceeding.
- Site-scoped checks additionally verify the target site is within the user's permitted sites.
- Unauthorized actions return an appropriate error and are not logged as audit events (only successful changes are audited).

## Dependencies

- **Active Directory / LDAP**: Source of user identity and group memberships.
- **Configuration Database (MS SQL)**: Stores LDAP group-to-role mappings and site scoping rules.
- **Configuration Database (via IAuditService)**: Security/admin changes (role mapping updates) are audit logged.

## Interactions

- **Central UI**: All UI requests pass through authentication and authorization.
- **Template Engine**: Designer role enforcement.
- **Deployment Manager**: Deployer role enforcement with site scoping.
- **Management Service / Central UI (secured writes)**: `RequireOperator` gates submission and `RequireVerifier` gates approval/rejection of MxGateway secured writes; the no-self-approval rule (Operator ≠ Verifier) is enforced server-side by the ManagementActor, not just in the UI.
- **All central components**: Role checks are a cross-cutting concern applied at the API layer.
- **Management Service**: The ManagementActor enforces role-based authorization on every incoming command using the authenticated user identity carried in the message envelope. The CLI authenticates users via the same LDAP bind mechanism and passes the user's identity (username, roles, permitted sites) in every request message. The ManagementActor applies the same role and site-scoping rules as the Central UI — no separate authentication path exists on the server side.
- **Transport (#24)**: Provides the `RequireDesign` policy (export) and `RequireAdmin` policy (import) enforced at both the Razor page layer and inside the `ZB.MOM.WW.ScadaBridge.Transport` service entrypoints.
