diff --git a/docs/components/CLI.md b/docs/components/CLI.md new file mode 100644 index 00000000..68ed2808 --- /dev/null +++ b/docs/components/CLI.md @@ -0,0 +1,250 @@ +# CLI + +The CLI is a standalone command-line tool that provides scripting and automation access to all ScadaBridge administrative operations. It connects to the central cluster's HTTP Management API and dispatches commands to the `ManagementActor` via `POST /management`. Authentication and role resolution happen server-side; the CLI sends LDAP credentials as HTTP Basic Auth on every request. + +## Overview + +The CLI component lives in `src/ZB.MOM.WW.ScadaBridge.CLI/` and builds to a self-contained `scadabridge` executable (or `scadabridge.exe` on Windows). It is not part of the Host binary — it deploys on any machine with HTTP access to a central node. + +The tool is built on `System.CommandLine` and exposes a hierarchical command tree organized by management domain (`template`, `instance`, `site`, `data-connection`, and so on). Every command follows the same pattern: resolve the management URL and credentials, construct a command object whose type maps to a `ManagementCommandRegistry` entry, serialize it into the `{ command, payload }` JSON envelope, and `POST` it to `/management`. The server response — JSON success body or `{ error, code }` error envelope — is printed to stdout or stderr respectively. + +The CLI is the preferred automation interface. When setting up system state (sites, templates, data connections, deployments, security) without the Central UI, use `scadabridge` commands rather than direct database manipulation. + +## Key Concepts + +### Connection to the active node + +The CLI connects through Traefik, the reverse proxy fronting the central cluster. Traefik routes each request to the active central node based on the `/health/active` probe, so the CLI does not need to know which node (`central-a` or `central-b`) is currently primary. Pointing `--url` (or `managementUrl`) at the Traefik address (default `http://localhost:9000` in Docker) provides automatic HA failover without any CLI-side configuration change. + +Direct-to-node access (`http://localhost:9001` for `central-a`, `http://localhost:9002` for `central-b`) bypasses Traefik and is useful for diagnostics but not for production automation. + +### Command dispatch + +Most commands serialize to a named management command and reach the `ManagementActor` through the `POST /management` endpoint. The `audit query` and `audit export` commands are the exception — they call plain REST endpoints (`GET /api/audit/query`, `GET /api/audit/export`) introduced by Audit Log (#23) and therefore use `SendGetAsync`/`SendGetStreamAsync` on `ManagementHttpClient` rather than `SendCommandAsync`. The streaming `audit export` path uses `HttpCompletionOption.ResponseHeadersRead` to avoid buffering large payloads in memory. + +### Authentication and roles + +The CLI encodes `--username` and `--password` as an HTTP Basic Auth header on every request. The server performs the LDAP bind, group lookup, and role resolution. The CLI never contacts LDAP directly and never caches credentials between invocations. + +Role enforcement is applied by the `ManagementActor`. Operations require the appropriate role: + +| Role | Covers | +|------|--------| +| `Admin` | Security settings, site management, SMTP config, audit-config queries | +| `Design` | Templates, shared scripts, external systems, DB connections, API methods, notification lists | +| `Deployment` | Instance lifecycle (deploy, enable, disable, delete), data connection bindings, bundle import/export | +| `OperationalAudit` | Reading the Audit Log (`audit query`) | +| `AuditExport` | Exporting the Audit Log (`audit export`) | + +A request lacking the required role exits with code `2` (authorization failure). A bad-credential response (HTTP 401) exits with code `1`. + +### Output formats + +Every command accepts `--format json` (default) or `--format table`. JSON output goes to stdout and is formatted with indented, camelCase JSON. Table output renders a padded plain-text table derived from the response JSON — arrays become rows, single objects become a two-column `Property / Value` table. Errors are always written as `{ "error": "...", "code": "..." }` to stderr, regardless of format. + +## Architecture + +`Program.cs` builds the `RootCommand` tree, attaches the four global options (`--url`, `--username`, `--password`, `--format`), and registers each command group: + +```csharp +rootCommand.Add(TemplateCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(InstanceCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(SiteCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(DeployCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(DataConnectionCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(ExternalSystemCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(NotificationCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(SecurityCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(AuditLogCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(AuditCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(HealthCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(DebugCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(SharedScriptCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(DbConnectionCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(ApiMethodCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +rootCommand.Add(BundleCommands.Build(urlOption, formatOption, usernameOption, passwordOption)); +``` + +Every command's action delegates to `CommandHelpers.ExecuteCommandAsync`, which owns the URL/credential resolution, `ManagementHttpClient` lifetime, and exit-code mapping. This consolidation means the authorization-failure exit code (`2`) is enforced uniformly — including for the `bundle` group, which uses `ExecuteCommandAsync` with a longer 5-minute timeout and a per-command `onSuccess` handler rather than a separate HTTP path. + +`CliConfig.Load()` is called at the start of every invocation. It merges `~/.scadabridge/config.json`, environment variables, and any defaults. A malformed or unreadable config file emits a warning to stderr and falls through to the environment variable / command-line precedence chain without crashing. + +## Usage + +### Running the CLI + +```bash +# Build +dotnet build src/ZB.MOM.WW.ScadaBridge.CLI + +# Minimal invocation (URL and credentials required) +scadabridge --url http://localhost:9000 --username multi-role --password password template list + +# Using Traefik (HA; routes to the active node automatically) +scadabridge --url http://localhost:9000 --username multi-role --password password site list + +# Table output +scadabridge --url http://localhost:9000 --username multi-role --password password \ + --format table instance list --site-id 1 +``` + +Credentials are safer supplied via environment variables than on the command line, where they appear in process listings and shell history: + +```bash +export SCADABRIDGE_MANAGEMENT_URL=http://localhost:9000 +export SCADABRIDGE_USERNAME=multi-role +export SCADABRIDGE_PASSWORD=password + +scadabridge template create --name "PumpStation" --description "Standard pump station" +scadabridge template attribute add --template-id 3 --name Speed --data-type Float --default-value 0 +scadabridge instance create --name "PS-01" --template-id 3 --site-id 1 +scadabridge instance deploy --id 7 +``` + +### Command groups + +| Group | Subcommands | Role required | +|-------|-------------|---------------| +| `template` | `list`, `get`, `create`, `update`, `delete`, `validate`; `attribute add/update/delete`; `alarm add/update/delete`; `script add/update/delete`; `composition add/delete`; `native-alarm-source add/list/remove` | `Design` | +| `instance` | `list`, `get`, `create`, `deploy`, `enable`, `disable`, `delete`, `set-bindings`; `native-alarm-source set/clear` | `Deployment` | +| `site` | `list`, `get`, `create`, `delete`, `deploy-artifacts`; `area list/create/update/delete` | `Admin` | +| `deploy` | `instance`, `artifacts`, `status` | `Deployment` | +| `data-connection` | `list`, `get`, `create`, `update`, `delete` | `Design` / `Deployment` | +| `external-system` | `list`, `get`, `create`, `update`, `delete` | `Design` | +| `notification` | `list`, `get`, `create`, `update`, `delete`; `smtp list/update` | `Design` / `Admin` | +| `security` | `api-key list/create/update/delete`; `role-mapping list/create/update/delete`; `scope-rule list/add/delete` | `Admin` | +| `shared-script` | `list`, `get`, `create`, `update`, `delete` | `Design` | +| `db-connection` | `list`, `get`, `create`, `update`, `delete` | `Design` | +| `api-method` | `list`, `get`, `create`, `update`, `delete` | `Design` | +| `bundle` | `export`, `preview`, `import` | `Deployment` | +| `audit` | `query`, `export`, `verify-chain` | `OperationalAudit` / `AuditExport` | +| `audit-config` | `query` (config-change audit trail; was `audit-log` pre-M8) | `Admin` | +| `health` | `summary`, `site`, `event-log`, `parked-messages` | `Deployment` | +| `debug` | `snapshot`, `stream` | `Deployment` | + +### Selected examples + +```bash +# Query the operational Audit Log for failed API outbound events in the last 24 hours +scadabridge audit query --since 24h --channel ApiOutbound --errors-only --format table + +# Export a full audit window to CSV +scadabridge audit export --since 2026-05-01T00:00:00Z --until 2026-06-01T00:00:00Z \ + --format csv --output /tmp/audit-may-2026.csv + +# Export a Transport bundle for selected templates (with transitive dependencies) +scadabridge bundle export --output /tmp/pump-station.scadabundle \ + --templates "PumpStation,BaseModule" --include-dependencies + +# Preview a bundle diff before importing +scadabridge bundle preview --input /tmp/pump-station.scadabundle + +# Import with overwrite conflict policy +scadabridge bundle import --input /tmp/pump-station.scadabundle --on-conflict overwrite + +# Stream live attribute and alarm changes for a running instance +scadabridge debug stream --id 7 + +# Query deployment records for a specific instance +scadabridge deploy status --instance-id 7 --page-size 20 +``` + +### Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | Command error (connection failure, validation error, server error) | +| `2` | Authorization failure (insufficient role; HTTP 403 or `FORBIDDEN`/`UNAUTHORIZED` error code) | + +## Configuration + +`~/.scadabridge/config.json` is loaded on every invocation. A malformed or unreadable file emits a warning to stderr; it does not abort the invocation. + +```json +{ + "managementUrl": "http://localhost:9000", + "defaultFormat": "json" +} +``` + +| Key | Default | Description | +|-----|---------|-------------| +| `managementUrl` | — | Base URL for the Management API. Overridden by `SCADABRIDGE_MANAGEMENT_URL` env var, then by `--url`. | +| `defaultFormat` | `json` | Default output format when `--format` is not supplied. Overridden by `SCADABRIDGE_FORMAT` env var, then by `--format`. | + +Credentials are intentionally never stored in the config file — they are sourced from environment variables or supplied per-invocation on the command line. Storing them in the file would persist them to disk in plaintext. + +### Environment variables + +| Variable | Overrides | Description | +|----------|-----------|-------------| +| `SCADABRIDGE_MANAGEMENT_URL` | `managementUrl` in config file | Management API base URL | +| `SCADABRIDGE_FORMAT` | `defaultFormat` in config file | Default output format | +| `SCADABRIDGE_USERNAME` | — | LDAP username; overridden by `--username` | +| `SCADABRIDGE_PASSWORD` | — | LDAP password; overridden by `--password`. Preferred over `--password` to avoid leaking credentials into process listings and shell history. | + +### URL precedence + +The management URL is resolved in this order: `--url` flag → `SCADABRIDGE_MANAGEMENT_URL` env var → `managementUrl` in config file. If none is set, the command exits with code `1` and a `NO_URL` error. + +## Dependencies & Interactions + +- [Management Service (#18)](./ManagementService.md) — the server-side counterpart. Every CLI command (except `audit query`/`audit export`) translates to a named management command dispatched through `POST /management` to the `ManagementActor`. Role enforcement and LDAP authentication are applied there. The `ManagementCommandRegistry` in Commons maps command types to their names; both sides must stay in sync. +- [Traefik Proxy (#20)](./TraefikProxy.md) — the recommended connection target. Pointing `--url` at the Traefik address ensures requests reach the active central node without per-command failover logic in the CLI. The `debug stream` command's WebSocket connection (SignalR `/hubs/debug-stream`) also traverses Traefik, which proxies the WebSocket upgrade natively. +- [Audit Log (#23)](./AuditLog.md) — the `audit` command group targets the `GET /api/audit/query` and `GET /api/audit/export` REST endpoints exposed by the Audit Log component, bypassing the management command envelope. The `audit-config` group (formerly `audit-log`) targets the configuration-change audit trail (`IAuditService`) via the standard management envelope. +- [Security & Auth (#10)](./Security.md) — the server resolves LDAP credentials and maps group memberships to ScadaBridge roles (`Admin`, `Design`, `Deployment`, `OperationalAudit`, `AuditExport`). The CLI does not interact with LDAP directly. +- [Commons (#16)](./Commons.md) — owns the management command record types and the `ManagementCommandRegistry` that maps each type to its wire name. The CLI project references Commons for these contracts. +- [Transport (#24)](./Transport.md) — the `bundle` command group drives the Transport feature: `bundle export` requests a base64-encoded bundle from the server and streams it to a local `.scadabundle` file; `bundle preview` uploads a file and returns the diff manifest; `bundle import` uploads a file and applies it with a configurable conflict policy. +- Design spec: [Component-CLI.md](../requirements/Component-CLI.md). + +## Troubleshooting + +### No management URL + +``` +{"error":"No management URL specified. Use --url, set SCADABRIDGE_MANAGEMENT_URL, or add 'managementUrl' to ~/.scadabridge/config.json.","code":"NO_URL"} +``` + +The URL is not set via `--url`, `SCADABRIDGE_MANAGEMENT_URL`, or the config file. Set one of these before running any command. + +### Connection failed + +``` +{"error":"Connection failed: Connection refused (localhost:9000)","code":"CONNECTION_FAILED"} +``` + +The central node or Traefik is not reachable at the configured URL. Verify the cluster is running and the URL matches the Traefik port (default `9000` in Docker) or the node's direct port (`9001`/`9002`). + +### Authorization failure (exit 2) + +The server returned HTTP 403 or an error code of `FORBIDDEN`/`UNAUTHORIZED`. The authenticated user's LDAP groups do not map to a role with permission for the requested operation. Use `security role-mapping list` (requires `Admin`) to inspect role mappings. The `multi-role` test user (`password`) holds `Admin`, `Design`, and `Deployment` roles. + +### Malformed config file warning + +``` +warning: ignoring malformed or unreadable /home/user/.scadabridge/config.json: ... +``` + +`CliConfig.Load()` caught a `JsonException`, `IOException`, or `UnauthorizedAccessException` reading the config file. The invocation continues using environment variables and command-line options. Fix or recreate the config file. + +### `audit-log` deprecation warning + +``` +Warning: 'audit-log' is deprecated and will be removed in a future release. Use 'audit-config' instead. +``` + +The `audit-log` command group was renamed to `audit-config` in M8 of Audit Log (#23). The old name still works but emits this warning to stderr. Migrate any scripts from `scadabridge audit-log ...` to `scadabridge audit-config ...`. + +### Bundle timeout + +`bundle export` and `bundle import` use a 5-minute per-command timeout (compared to the 30-second default). If a bundle operation times out, the server-side export or import may still be running. Re-try with a smaller selection or check the central node logs. + +## Related Documentation + +- [CLI design specification](../requirements/Component-CLI.md) +- [Management Service](./ManagementService.md) +- [Traefik Proxy](./TraefikProxy.md) +- [Audit Log](./AuditLog.md) +- [Security](./Security.md) +- [Commons](./Commons.md) diff --git a/docs/components/CentralUI.md b/docs/components/CentralUI.md new file mode 100644 index 00000000..e076170c --- /dev/null +++ b/docs/components/CentralUI.md @@ -0,0 +1,223 @@ +# Central UI + +The Central UI is a Blazor Server web application that hosts every management, configuration, deployment, monitoring, and audit workflow for the ScadaBridge system. It runs on the central cluster only; sites have no user interface. + +## Overview + +Central UI (#9) is built on ASP.NET Core + Blazor Server with Bootstrap CSS. All UI logic executes server-side; updates reach the browser through Blazor's built-in SignalR circuit. No third-party component frameworks are used — tables, grids, forms, and custom controls are implemented directly as Blazor + Bootstrap components. + +The component code lives in `src/ZB.MOM.WW.ScadaBridge.CentralUI/`, split into: + +- `Auth/` — cookie authentication state provider, login/logout/ping Minimal API endpoints, site-scope service. +- `Audit/` — CSV export Minimal API endpoint. +- `Components/Layout/` — `MainLayout`, `LoginLayout`, and `NavMenu` (the policy-gated rail navigation). +- `Components/Pages/` — pages, grouped by nav section: `Admin/`, `Audit/`, `Deployment/`, `Design/`, `Monitoring/`, `Notifications/`, `SiteCalls/`. +- `Components/Shared/` — reusable non-page components (`DataTable`, `MonacoEditor`, `ToastNotification`, `SessionExpiry`, dialog infrastructure, and others). +- `Components/Health/` — KPI tile components for Notification Outbox, Site Calls, and Audit Log. +- `Components/Audit/` — the `AuditFilterBar`, `AuditResultsGrid`, `AuditDrilldownDrawer`, and execution-tree components used by the Audit Log page. +- `ScriptAnalysis/` — Roslyn-backed script analysis service and Minimal API endpoint (`/api/centralui/script/analyze`) used by the Monaco editor. +- `Services/` — scoped UI services: `AuditLogQueryService`, `AuditLogExportService`, `BrowseService`, `BindingTester`, and `SiteScopeService`. + +The single DI entry point is `ServiceCollectionExtensions.AddCentralUI`, registered only by the central-role Host composition root. `EndpointExtensions.MapCentralUI` wires the Minimal API endpoints and the Razor component routes onto the ASP.NET Core pipeline. + +## Key Concepts + +### Authentication and session model + +Authentication is a standard LDAP bind via `ILdapAuthService` (from Security, #10). On success, `POST /auth/login` maps the user's LDAP groups to ScadaBridge roles via `IGroupRoleMapper`, constructs a `ClaimsIdentity` carrying role claims and site-scope `SiteId` claims, and calls `context.SignInAsync` to write an HttpOnly/Secure ASP.NET Core cookie with `IsPersistent = true` and `SlidingExpiration = true`. No fixed `ExpiresUtc` is stamped — the idle timeout is owned by the cookie middleware configured in the Security component. + +`POST /auth/token` offers the same LDAP flow and returns a JWT bearer token for the CLI. + +Because Blazor Server's `HttpContext` is only valid during the initial HTTP request that establishes the SignalR circuit, `CookieAuthenticationStateProvider` snapshots the authenticated `ClaimsPrincipal` once at construction time and serves that snapshot for the entire circuit lifetime: + +```csharp +public CookieAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor) +{ + var user = httpContextAccessor.HttpContext?.User + ?? new ClaimsPrincipal(new ClaimsIdentity()); + + _circuitAuthState = Task.FromResult(new AuthenticationState(user)); +} + +public override Task GetAuthenticationStateAsync() + => _circuitAuthState; +``` + +Reading `IHttpContextAccessor` on each `GetAuthenticationStateAsync` call would return `null` (or a stale context) for the lifetime of a long-lived circuit, causing `` re-renders to see an unauthenticated principal. + +### Session expiry detection + +Because `CookieAuthenticationStateProvider` serves a frozen principal, the circuit can never observe a server-side cookie expiry by polling Blazor's auth state. `SessionExpiry` (a headless component rendered in `MainLayout`) polls `GET /auth/ping` via a JavaScript `fetch` call every minute. The ping endpoint returns `200` while the cookie is still valid and `401` once it lapses. A `401` redirects the browser to `/login` with a full-page navigation. The cookie middleware re-validates (and slides) the cookie on every ping, so the poll itself does not artificially extend a genuinely idle session beyond the configured timeout. + +### Site-scoped authorization + +`SiteScopeService` (scoped, registered in `AddCentralUI`) reads `SiteId` claims from the circuit principal. Deployment users whose LDAP mapping carries site-scope rules carry one integer `SiteId` claim per permitted site; Admin and Design users carry none (system-wide). Pages that enumerate sites or issue cross-site commands call `SiteScopeService.FilterSitesAsync` and `IsSiteAllowedAsync` to enforce the grant before any operation. + +### Repository access pattern + +CentralUI pages read and write the configuration database directly through `ICentralUiRepository` and other scoped EF Core repositories — there is no Akka round-trip for design-time CRUD. Operations that must reach site actors (deployment commands, debug stream subscriptions, OPC UA browse, KPI queries) go through `CommunicationService` (from Communication, #5). + +## Architecture + +### Layout and navigation + +`MainLayout` wraps every authenticated page in `ThemeShell` (from the shared `ZB.MOM.WW.Theme` package), which provides the brand bar, responsive hamburger, and side-rail chassis. `NavMenu` fills the rail's `