256 lines
16 KiB
Markdown
256 lines
16 KiB
Markdown
# Component: Inbound API
|
|
|
|
## Purpose
|
|
|
|
The Inbound API exposes a web API on the central cluster that external systems can call into. This is the reverse of the External System Gateway — where that component handles the SCADA system calling out to external systems, this component handles external systems calling in. It provides API key authentication, method-level authorization, and script-based method implementations.
|
|
|
|
## Location
|
|
|
|
Central cluster only (active node). Not available at site clusters.
|
|
|
|
## Responsibilities
|
|
|
|
- Host a web API endpoint on the central cluster.
|
|
- Authenticate inbound requests via API keys.
|
|
- Route requests to the appropriate API method definition.
|
|
- Enforce per-method API key authorization (only approved keys can call a given method).
|
|
- Execute the C# script implementation for the called method.
|
|
- Return structured responses to the caller.
|
|
- Failover: API becomes available on the new active node after central failover.
|
|
|
|
## API Key Management
|
|
|
|
### Storage
|
|
- API keys are stored in the **configuration database (MS SQL)**.
|
|
|
|
### Key Properties
|
|
- **Name/Label**: Human-readable identifier for the key (e.g., "MES-Production", "RecipeManager-Dev").
|
|
- **Key Value**: The secret key string used for authentication.
|
|
- **Enabled/Disabled Flag**: Keys can be disabled without deletion.
|
|
|
|
### Management
|
|
- Managed by users with the **Administrator** role via the Central UI.
|
|
- All key changes (create, enable/disable, delete) are audit logged.
|
|
|
|
## API Method Definition
|
|
|
|
### Properties
|
|
Each API method definition includes:
|
|
- **Method Name**: Unique identifier and URL path segment for the endpoint.
|
|
- **Approved API Keys**: List of API keys authorized to invoke this method. Requests from non-approved keys are rejected.
|
|
- **Parameter Definitions**: Ordered list of input parameters, each with:
|
|
- Parameter name.
|
|
- Data type — the **extended type system** (Boolean, Integer, Float, String, plus the nestable Object and List; see [Extended Type System](#extended-type-system)).
|
|
- Whether the parameter is required.
|
|
- **Return Value Definition**: Structure of the response, with:
|
|
- Field names and (extended-system) data types. Supports returning **lists of objects** and arbitrarily nested structures.
|
|
- **Implementation Script**: C# script that executes when the method is called. Stored **inline** in the method definition. Follows standard C# authoring patterns but has no template inheritance — it is a standalone script tied to this method.
|
|
- **Timeout**: Configurable per method. Defines the maximum time the method is allowed to execute (including any routed calls to sites) before returning a timeout error to the caller.
|
|
|
|
### Management
|
|
- Managed by users with the **Designer** role via the Central UI.
|
|
- All method definition changes are audit logged.
|
|
|
|
## HTTP Contract
|
|
|
|
### URL Structure
|
|
- All API calls use `POST /api/{methodName}`.
|
|
- Method names map directly to URL path segments (e.g., method "GetProductionReport" → `POST /api/GetProductionReport`).
|
|
- All calls are POST — these are RPC-style script invocations, not RESTful resource operations.
|
|
|
|
### Authentication Header
|
|
- The API key token (`sbk_<keyId>_<secret>`) is accepted from **either** of two headers:
|
|
- `Authorization: Bearer sbk_<keyId>_<secret>` — the preferred form.
|
|
- `X-API-Key: sbk_<keyId>_<secret>` — alternate form for callers that cannot set the Authorization header.
|
|
- When both headers are present, `Authorization` takes precedence.
|
|
- Both forms are processed by the same `IApiKeyVerifier` from the shared `ZB.MOM.WW.Auth.ApiKeys` library; the verifier strips the optional `Bearer ` prefix and performs the same peppered-HMAC constant-time verification regardless of which header was used.
|
|
|
|
### Request Format
|
|
- Content-Type: `application/json`.
|
|
- Parameters are top-level JSON fields in the request body matching the method's parameter definitions:
|
|
```json
|
|
{
|
|
"siteId": "SiteA",
|
|
"startDate": "2026-03-01",
|
|
"endDate": "2026-03-16"
|
|
}
|
|
```
|
|
|
|
### Response Format
|
|
- **Success (200)**: The response body is the method's return value as JSON, with fields matching the return value definition:
|
|
```json
|
|
{
|
|
"siteName": "Site Alpha",
|
|
"totalUnits": 14250,
|
|
"lines": [
|
|
{ "lineName": "Line-1", "units": 8200, "efficiency": 92.5 },
|
|
{ "lineName": "Line-2", "units": 6050, "efficiency": 88.1 }
|
|
]
|
|
}
|
|
```
|
|
- **Failure (4xx/5xx)**: The response body is an error object:
|
|
```json
|
|
{
|
|
"error": "Site unreachable",
|
|
"code": "SITE_UNREACHABLE"
|
|
}
|
|
```
|
|
- HTTP status codes distinguish success from failure — no envelope wrapper.
|
|
|
|
### Extended Type System
|
|
- API method parameter and return type definitions support an **extended type system** beyond the four template attribute types (Boolean, Integer, Float, String):
|
|
- **Object**: A named structure with typed fields. Supports nesting.
|
|
- **List**: An ordered collection of objects or primitive types.
|
|
- This allows complex request/response structures (e.g., an object containing properties and a list of nested objects).
|
|
- Template attributes retain the simpler four-type system. The extended types apply only to Inbound API method definitions and External System Gateway method definitions.
|
|
|
|
#### Type Definition Format & Nested Validation
|
|
|
|
- Parameter and return type definitions are persisted as **JSON Schema** (the canonical format produced by the Central UI schema builder; see the `MigrateParametersToJsonSchema` migration). An object declares its fields via `properties` (+ a `required` array); a list declares its element type via `items`. The legacy flat-array form (`[{name,type,required,itemType?}]`) is still accepted on read for transition safety.
|
|
- Validation is **recursive and type-aware** for the extended types (request parameters and script return values alike, via a single shared engine so the two cannot drift):
|
|
- **Object**: each declared field's value is validated against its declared (possibly nested) type; a missing required field and a present-but-wrong type are both reported.
|
|
- **List**: every element is validated against the declared element type (recursing into nested objects/lists). A list whose element type is left undeclared (`array` without `items`) is shape-checked only.
|
|
- **Scalars at any depth** are checked against the extended type.
|
|
- Errors are **path-qualified** (e.g. `order.items[2].quantity`) so the caller can locate the offending field.
|
|
- **Undeclared fields are rejected** at every level (consistent with the top-level "unexpected parameter" rejection): an object that declares its fields rejects any field not in its `properties`, so a typo'd field name surfaces as a `400`/error rather than being silently ignored. A bare object schema with no declared fields (`{"type":"object"}`) stays shape-only and accepts any fields.
|
|
- A JSON `null` value satisfies any declared type (a present-but-null field is allowed); only the **absence** of a required field is an error.
|
|
|
|
## Script Compilation & Hot-Reload
|
|
|
|
API method scripts are compiled at central startup — all method definitions are loaded from the configuration database and compiled into in-memory delegates.
|
|
|
|
### Update Workflow
|
|
|
|
- Updating a method via the CLI (`api-method update --id <N> --code '...'`) or Management API triggers immediate recompilation (`CompileAndRegister`). The updated script takes effect on the next API call — no node restart is required.
|
|
- Creating a new method after startup: if the method is created but not yet compiled, the first invocation triggers lazy (on-demand) compilation.
|
|
|
|
### Direct SQL Warning
|
|
|
|
> **Do not edit API method scripts via direct SQL.** The in-memory compiled script will not be updated until the next node restart. Always use the CLI, Management API, or Central UI to modify API method scripts.
|
|
|
|
---
|
|
|
|
## API Call Logging
|
|
|
|
- **Every request — success or failure — emits one `ApiInbound.Completed` row** to `ICentralAuditWriter` from request middleware before the HTTP response is flushed. The row captures the API key **name** (never the key material), remote IP, user-agent, response status, duration, and the request/response bodies. Bodies are captured in full up to `AuditLog:InboundMaxBytes` (default 1 MiB); `PayloadTruncated = 1` only when that ceiling is hit. Header redaction and per-target body redactors still apply (see Component-AuditLog.md, Payload Capture Policy). This supersedes the earlier failures-only stance: operational API traffic is now part of the centralized audit log, so configuration changes and call activity share a single retention/query surface.
|
|
- Script execution errors (500 responses) remain captured on the same `ApiInbound.Completed` row (response status + error fields) rather than emitting a separate failure-only event.
|
|
- **Fail-soft semantics (fire-and-forget).** The audit write is **fire-and-forget** (`InboundAPI-018`): `WriteAsync` is called without `await` so the user-facing HTTP response is never blocked or delayed by the audit path. Asynchronous faults are observed via a `ContinueWith` continuation that logs at Warning and increments `CentralAuditWriteFailures` (see Health Monitoring #11) rather than being silently dropped. A failed audit append never turns a successful API call into an error returned to the caller.
|
|
- No rate limiting — this is a private API in a controlled industrial environment with a known set of callers. Misbehaving callers are handled operationally (disable the API key).
|
|
|
|
## Request Flow
|
|
|
|
```mermaid
|
|
%%{init: {'theme':'base', 'themeVariables': {'textColor':'#111111','lineColor':'#555555','edgeLabelBackground':'#ffffff','fontSize':'15px'}}}%%
|
|
flowchart TD
|
|
ext(["External System"])
|
|
api["Inbound API (Central)"]
|
|
s1["1. Extract API key from request"]
|
|
s2["2. Validate key exists and is enabled"]
|
|
s3["3. Resolve method by name"]
|
|
s4["4. Check API key is in method's approved list"]
|
|
s5["5. Validate and deserialize parameters"]
|
|
s6["6. Execute implementation script<br/>(subject to method timeout)"]
|
|
s7["7. Serialize return value"]
|
|
s8["8. Return response"]
|
|
|
|
ext --> api
|
|
api --> s1
|
|
s1 --> s2
|
|
s2 --> s3
|
|
s3 --> s4
|
|
s4 --> s5
|
|
s5 --> s6
|
|
s6 --> s7
|
|
s7 --> s8
|
|
|
|
classDef start fill:#d5e8d4,stroke:#82b366,color:#111111;
|
|
classDef proc fill:#dae8fc,stroke:#6c8ebf,color:#111111;
|
|
classDef dec fill:#fff2cc,stroke:#d6b656,color:#111111;
|
|
classDef warn fill:#ffe6cc,stroke:#d79b00,color:#111111;
|
|
classDef alt fill:#e1d5e7,stroke:#9673a6,color:#111111;
|
|
class ext start
|
|
class api proc
|
|
class s1,s2,s3,s4,s5,s7 dec
|
|
class s6 alt
|
|
class s8 warn
|
|
```
|
|
|
|
## Implementation Script Capabilities
|
|
|
|
The C# script that implements an API method executes on the central cluster. Unlike instance scripts at sites, inbound API scripts run on central and can interact with **any instance at any site** through a routing API.
|
|
|
|
Inbound API scripts **cannot** call shared scripts directly — shared scripts are deployed to sites only and execute inline in Script Actors. To execute logic on a site, use `Route.To().Call()`.
|
|
|
|
### Script Runtime API
|
|
|
|
#### Instance Routing
|
|
- `Route.To("instanceUniqueCode").Call("scriptName", parameters)` — Invoke a script on a specific instance at any site. Central routes the call to the appropriate site via the Communication Layer. The call reaches the target Instance Actor's Script Actor, which spawns a Script Execution Actor to execute the script. The return value flows back to the calling API script.
|
|
- `Route.To("instanceUniqueCode").GetAttribute("attributeName")` — Read a single attribute value from a specific instance at any site.
|
|
- `Route.To("instanceUniqueCode").GetAttributes("attr1", "attr2", ...)` — Read multiple attribute values in a **single call**, returned as a dictionary of name-value pairs.
|
|
- `Route.To("instanceUniqueCode").SetAttribute("attributeName", value)` — Write a single attribute value on a specific instance at any site.
|
|
- `Route.To("instanceUniqueCode").SetAttributes(dictionary)` — Write multiple attribute values in a **single call**, accepting a dictionary of name-value pairs.
|
|
|
|
#### Input/Output
|
|
- **Input parameters** are available as defined in the method definition.
|
|
- **Return value** construction matching the defined return structure.
|
|
|
|
#### Parameter Access
|
|
- `Parameters["key"]` — Raw dictionary access.
|
|
- `Parameters.Get<T>("key")` — Typed access (same API as site runtime scripts). See Site Runtime component for full type support.
|
|
|
|
> **No direct database access.** Inbound API scripts are not given a raw database
|
|
> client. Handing a script a raw `SqlConnection` is in direct tension with the
|
|
> ScadaBridge script trust model (scripts are forbidden `System.IO`, `Process`,
|
|
> `Threading`, `Reflection`, and raw network access). The `ForbiddenApiChecker`
|
|
> statically enforces this by delegating to the shared `ScriptAnalysis`
|
|
> `ScriptTrustValidator` (component #25), which is the single authoritative
|
|
> source of truth for the forbidden-API policy. The unified policy permits
|
|
> `System.Diagnostics.Stopwatch`/`Debug` while retaining the `Process`-only ban,
|
|
> and adds reflection-gateway member and `dynamic`/`Activator` hardening.
|
|
> This is defence-in-depth static enforcement, not a true runtime sandbox. Scripts
|
|
> interact with the system only through the curated `Route` and `Parameters`
|
|
> surfaces above. If a method needs data from the configuration or machine-data
|
|
> databases, that access belongs behind a dedicated, scoped helper — not a
|
|
> general-purpose connection — and would be added here as an explicit design change.
|
|
|
|
### Routing Behavior
|
|
- The `Route.To()` helper resolves the instance's site assignment from the configuration database and routes the request to the correct site cluster via the Communication Layer.
|
|
- The call is **synchronous from the API caller's perspective** — the API method blocks until the site responds or the **method-level timeout** is reached.
|
|
- If the target site is unreachable or the call times out, the call fails and the API returns an error to the caller. No store-and-forward buffering is used for inbound API calls.
|
|
|
|
## Authentication Details
|
|
|
|
- The API key token is accepted from **either** the `Authorization: Bearer <token>` header (preferred) **or** the `X-API-Key: <token>` header (alternate). `Authorization` takes precedence when both are present.
|
|
- The token format is `sbk_<keyId>_<secret>`. Both header forms are verified by the same `IApiKeyVerifier` from `ZB.MOM.WW.Auth.ApiKeys`.
|
|
- The system validates:
|
|
1. The token is well-formed and the key ID exists in the key store.
|
|
2. The key is enabled (not revoked).
|
|
3. The key's provisioned scopes include the requested method name (case-sensitive).
|
|
- Failed authentication returns an appropriate HTTP error (401 Unauthorized for invalid/missing credentials; 403 Forbidden for a valid key not approved for the method). For enumeration-safety, both "method not found" and "key not in scope" return 403 with an identical body.
|
|
|
|
## Error Handling
|
|
|
|
- Invalid API key → 401 Unauthorized.
|
|
- Valid key but not approved for method → 403 Forbidden.
|
|
- Invalid parameters → 400 Bad Request.
|
|
- Script execution failure → 500 Internal Server Error (with safe error message, no internal details exposed).
|
|
- Script errors are logged in the central audit/event system.
|
|
|
|
## Dependencies
|
|
|
|
- **Configuration Database (MS SQL)**: Stores API keys and method definitions.
|
|
- **Communication Layer**: Routes requests to sites when method implementations need site data.
|
|
- **Security & Auth**: API key validation (separate from LDAP/AD — API uses key-based auth).
|
|
- **Configuration Database (via IAuditService)**: All API key and method definition changes are audit logged.
|
|
- **Script Analysis (#25)**: `ForbiddenApiChecker` delegates to `ScriptTrustValidator.FindViolations` for the authoritative forbidden-API verdict during script compilation and validation.
|
|
- **Audit Log (#23)**: Every inbound API request emits an `ApiInbound.Completed` row via `ICentralAuditWriter` from request middleware (non-blocking for the HTTP response). Request and response bodies are captured in full up to `AuditLog:InboundMaxBytes` (default 1 MiB) per the Audit Log Payload Capture Policy; redaction (headers + per-target body redactors) still applies before persistence.
|
|
- **Cluster Infrastructure**: API is hosted on the active central node and fails over with it.
|
|
|
|
## Interactions
|
|
|
|
- **External Systems**: Call the API with API keys.
|
|
- **Communication Layer**: API method scripts use this to reach sites.
|
|
- **Site Runtime (Instance Actors, Script Actors)**: Routed calls execute on site Instance Actors via their Script Actors.
|
|
- **Central UI**: Administrator manages API keys; Designer manages method definitions.
|
|
- **Configuration Database (via IAuditService)**: Configuration changes are audited.
|