From 2be0290e173b2ab12f24c732ede66b036ae027e4 Mon Sep 17 00:00:00 2001 From: Jilles van Gurp Date: Wed, 1 Apr 2026 20:15:32 +0200 Subject: [PATCH] Sync Open Location Hub docs into website --- .../_index.md} | 4 + content/open-location-hub/docs/_index.md | 16 ++ .../open-location-hub/docs/architecture.md | 100 ++++++++ content/open-location-hub/docs/auth.md | 236 ++++++++++++++++++ .../open-location-hub/docs/configuration.md | 110 ++++++++ content/open-location-hub/docs/rpc.md | 224 +++++++++++++++++ i18n/en.yaml | 3 + layouts/_default/baseof.html | 2 + layouts/_default/list.html | 51 ++++ 9 files changed, 746 insertions(+) rename content/{open-location-hub.md => open-location-hub/_index.md} (96%) create mode 100644 content/open-location-hub/docs/_index.md create mode 100644 content/open-location-hub/docs/architecture.md create mode 100644 content/open-location-hub/docs/auth.md create mode 100644 content/open-location-hub/docs/configuration.md create mode 100644 content/open-location-hub/docs/rpc.md create mode 100644 layouts/_default/list.html diff --git a/content/open-location-hub.md b/content/open-location-hub/_index.md similarity index 96% rename from content/open-location-hub.md rename to content/open-location-hub/_index.md index e1c3e1d..50ccd64 100644 --- a/content/open-location-hub.md +++ b/content/open-location-hub/_index.md @@ -13,6 +13,8 @@ It is the integration-layer counterpart to Open Location Stack's mapping work: o The current repository is still evolving quickly, but the intended direction is already clear: a production-grade, open source hub that teams can run, extend, adapt, and integrate without being locked into vendor-specific middleware. +The software documentation for the current implementation is published at [Open Location Hub Docs](/open-location-hub/docs/). That section is generated from the repository's `docs/` directory and is intended to stay aligned with the code as the project evolves. + ## Business value Open Location Hub is intended to give vendors, integrators, and enterprise teams an open integration backbone they can actually use in commercial delivery models. @@ -94,6 +96,8 @@ This project is still early and not yet feature complete. It should be treated a If you care about interoperable RTLS infrastructure, now is the right time to get involved. Try the code, review the API direction, open issues, and contribute pull requests to help shape the implementation. +[Browse the generated docs](/open-location-hub/docs/) + [Learn about Floor Plan Editor](/floor-plan-editor/) [View the repository on GitHub](https://github.com/Open-Location-Stack/open-location-hub) diff --git a/content/open-location-hub/docs/_index.md b/content/open-location-hub/docs/_index.md new file mode 100644 index 0000000..b196ca1 --- /dev/null +++ b/content/open-location-hub/docs/_index.md @@ -0,0 +1,16 @@ +--- +title: "Software Documentation" +description: "Use the documents in this directory for software behavior, runtime configuration, and integration guidance." +draft: false +generated: true +generated_from: "docs/index.md" +github_url: "https://github.com/Open-Location-Stack/open-location-hub/blob/main/docs/index.md" +--- +_This page is generated from the Open Location Hub source documentation and should not be edited in the website repository._ + +Use the documents in this directory for software behavior, runtime configuration, and integration guidance. + +- `architecture.md`: system structure, processing flows, and trust boundaries +- `configuration.md`: environment variables and runtime tuning +- `auth.md`: JWT auth modes, authorization model, and permission file behavior +- `rpc.md`: REST-facing RPC usage and control-plane behavior diff --git a/content/open-location-hub/docs/architecture.md b/content/open-location-hub/docs/architecture.md new file mode 100644 index 0000000..884586a --- /dev/null +++ b/content/open-location-hub/docs/architecture.md @@ -0,0 +1,100 @@ +--- +title: "Architecture" +description: "Generated documentation page for Architecture." +draft: false +generated: true +generated_from: "docs/architecture.md" +github_url: "https://github.com/Open-Location-Stack/open-location-hub/blob/main/docs/architecture.md" +--- +_This page is generated from the Open Location Hub source documentation and should not be edited in the website repository._ + +## Layers +- `cmd/hub`: process bootstrap and wiring +- `internal/config`: environment-driven configuration +- `internal/httpapi`: API surface and handlers +- `internal/ws`: OMLOX WebSocket wrapper protocol, subscriptions, and fan-out +- `internal/storage/postgres`: durable store +- `internal/mqtt`: MQTT topic mapping and broker integration +- `internal/auth`: token verification middleware +- `internal/rpc`: local-method dispatch, MQTT RPC bridging, announcements, and aggregation +- `internal/hub`: shared CRUD, ingest, derived event generation, collision evaluation, and internal event bus emission + +## Metadata And Hot State +- Postgres is the durable source of truth for hub metadata, zones, fences, trackables, and location providers. +- The runtime resolves the singleton hub metadata row before the service starts so one stable `hub_id` and label are available for startup validation, internal event provenance, and identify responses. +- The hub loads those resources into an immutable in-memory metadata snapshot before it accepts traffic. +- Successful CRUD writes update Postgres first, then update the in-memory snapshot, invalidate any affected derived metadata such as zone transforms, and emit a `metadata_changes` bus event. +- A background reconcile loop reloads durable metadata periodically and emits the same `metadata_changes` notifications when it detects out-of-band create, update, or delete drift. +- Decision-critical ingest state is kept in process memory: + - dedup windows + - latest provider-source locations + - latest trackable locations and WGS84 motions + - proximity hysteresis state + - fence membership state + - collision pair state + +## Event Fan-Out +1. REST, MQTT, or WebSocket ingest enters the shared hub service. +2. The hub validates, normalizes, deduplicates, updates in-memory transient state, and derives follow-on events. +3. The hub emits normalized internal events for locations, proximities, trackable motions, fence events, optional collision events, and metadata changes. +4. MQTT and WebSocket consume that same event stream and publish transport-specific payloads. + +Implications: +- ingest logic is shared across REST, MQTT, and WebSocket +- MQTT is no longer the only downstream publication path +- the internal event seam decouples downstream publication from MQTT-specific topics +- hub-issued UUIDs for REST-managed resources, derived fence/collision events, and RPC caller IDs now use UUIDv7 so emitted identifiers are time-sortable +- internal hub events carry the persisted `origin_hub_id` so downstream transports preserve source provenance + +## RPC Control Plane +1. A client calls `GET /v2/rpc/available` or `PUT /v2/rpc` over HTTP. +2. REST auth verifies the bearer token and route-level access. +3. The RPC bridge applies method-level authorization for discovery or invocation. +4. The bridge looks up the method in a unified registry containing: + - hub-owned local methods + - MQTT-discovered external handlers +5. The bridge either: + - handles the method locally + - forwards it to MQTT + - or does both and aggregates responses +6. The bridge returns a JSON-RPC result or JSON-RPC error payload to the HTTP caller. + +Built-in identify behavior: +- `com.omlox.identify` returns the persisted hub label as its `name` +- `com.omlox.identify` also returns the stable persisted `hub_id` + +Trust boundaries: +- HTTP clients should talk to the hub, not directly to MQTT devices +- MQTT should be restricted to the hub and trusted device/adaptor components +- the hub is the policy, audit, and handler-selection boundary for control-plane actions + +## Proximity Resolution Path +1. A REST, WebSocket, or MQTT `Proximity` update enters the shared hub service. +2. The hub resolves the referenced zone by `zone.id` or `zone.foreign_id`. +3. Only proximity-oriented zones are accepted for this path (`rfid` and `ibeacon`). +4. The hub loads transient per-provider proximity state from the in-memory processing state. +5. The resolver applies hub defaults plus any `Zone.properties.proximity_resolution` overrides. +6. Hysteresis rules decide whether to stay in the current zone or switch to the new candidate zone. +7. The hub emits a derived local `Location` using the resolved zone position and then continues through the normal location pipeline. + +Resolver notes: +- durable configuration lives in Postgres as part of the zone resource +- transient proximity membership state lives in the in-memory processing state +- derived location metadata includes hub extension fields such as `resolution_method`, `resolved_zone_id`, and `sticky` + +Resolver scope: +- the resolver emits the configured zone position as the derived point +- proximity resolution supports static proximity zones +- resolution policy is driven by hub defaults and zone-specific overrides + +## Contract-first flow +1. Update OpenAPI spec. +2. Regenerate generated server/types. +3. Implement handler behavior. +4. Validate with tests and check pipeline. + +## WebSocket Notes +- `GET /v2/ws/socket` is implemented outside the REST OpenAPI contract because it is a protocol companion surface rather than a generated REST endpoint. +- When auth is enabled, WebSocket messages authenticate with `params.token` and apply dedicated topic publish/subscribe authorization. +- `collision_events` is a known topic but remains configuration-gated by `COLLISIONS_ENABLED`. +- `metadata_changes` is a subscribe-only topic that carries lightweight metadata replication notifications shaped as `{id,type,operation,timestamp}`. diff --git a/content/open-location-hub/docs/auth.md b/content/open-location-hub/docs/auth.md new file mode 100644 index 0000000..bdeec1c --- /dev/null +++ b/content/open-location-hub/docs/auth.md @@ -0,0 +1,236 @@ +--- +title: "Authentication and Authorization" +description: "This project supports standards-based JWT bearer authentication for the REST API and an authorization model built around JWT claims plus a server-side permissions file." +draft: false +generated: true +generated_from: "docs/auth.md" +github_url: "https://github.com/Open-Location-Stack/open-location-hub/blob/main/docs/auth.md" +--- +_This page is generated from the Open Location Hub source documentation and should not be edited in the website repository._ + +This project supports standards-based JWT bearer authentication for the REST API and an authorization model built around JWT claims plus a server-side permissions file. + +The same token verifier is also used for the OMLOX WebSocket surface, but WebSocket authentication happens per message through `params.token` instead of the HTTP `Authorization` header. + +## Modes + +- `none`: disable auth checks +- `oidc`: verify bearer tokens through OIDC discovery and JWKS +- `static`: verify bearer tokens against static PEM keys or JWKS URLs +- `hybrid`: accept either OIDC-verified or static-key tokens + +## OIDC and JWKS + +For `oidc` mode, the hub loads issuer metadata from `AUTH_ISSUER`, discovers the provider JWKS endpoint, and verifies JWT signatures and standard claims. Provider metadata and verifier state are cached and refreshed according to `AUTH_OIDC_REFRESH_TTL` instead of being reloaded on every request. + +Relevant settings: + +- `AUTH_ISSUER` +- `AUTH_AUDIENCE` +- `AUTH_ALLOWED_ALGS` +- `AUTH_CLOCK_SKEW` +- `AUTH_HTTP_TIMEOUT` +- `AUTH_OIDC_REFRESH_TTL` + +## Authorization Model + +Authorization uses a role and ownership based model: + +- authenticate the bearer token first +- extract a role-like claim from the JWT via `AUTH_ROLES_CLAIM` +- load path permissions from `AUTH_PERMISSIONS_FILE` +- optionally enforce ownership checks with the claim configured by `AUTH_OWNED_RESOURCES_CLAIM` + +Supported permission values: + +- `CREATE_ANY` +- `READ_ANY` +- `UPDATE_ANY` +- `DELETE_ANY` +- `CREATE_OWN` +- `READ_OWN` +- `UPDATE_OWN` +- `DELETE_OWN` + +Method mapping: + +- `GET` and `HEAD` require `READ_*` +- `POST` requires `CREATE_*` +- `PUT` and `PATCH` require `UPDATE_*` +- `DELETE` requires `DELETE_*` + +`*_OWN` permissions apply to routes that include explicit path identifiers, such as `/v2/providers/:providerId`. Collection routes such as `/v2/zones` use the corresponding `*_ANY` semantics. + +## Permissions File + +The permissions file is YAML. Top-level keys are values from the configured role claim. In production this would usually be a role or group claim. For the included Dex development fixture, the role claim is set to `email` because Dex's local password database produces deterministic user identity claims without extra role mapping. That is a development convenience, not a production recommendation. + +Example: + +```yaml +admin@example.com: + description: Full access + /v2/*: + - CREATE_ANY + - READ_ANY + - UPDATE_ANY + - DELETE_ANY + rpc: + discover: true + invoke: + "*": true + +reader@example.com: + description: Read-only access + /v2/zones: + - READ_ANY + /v2/zones/:zoneId: + - READ_ANY + /v2/rpc/available: + - READ_ANY + rpc: + discover: true + invoke: + com.omlox.ping: true + com.omlox.identify: true +``` + +Path placeholders are used for ownership checks. The hub derives claim keys from route parameter names. For example `:providerId` maps to `provider_ids`. + +RPC policy entries are evaluated after route-level authorization. They use a +dedicated `rpc` section per role: + +- `discover: true` allows `GET /v2/rpc/available` +- `invoke` lists allowed method names +- `invoke` entries may be: + - exact method names such as `com.omlox.ping` + - prefix wildcards such as `com.vendor.*` + - `*` for full RPC invocation access + +This means a role can be allowed to reach the RPC endpoint path but still be +blocked from invoking a specific method. + +WebSocket policy entries are evaluated separately from REST route permissions. They use a dedicated `websocket` section per role: + +- `subscribe` lists topic names or wildcard patterns the role may subscribe to +- `publish` lists topic names or wildcard patterns the role may send `message` events to + +Example: + +```yaml +admin@example.com: + websocket: + subscribe: + "*": true + publish: + location_updates: true + proximity_updates: true +``` + +The WebSocket policy matcher supports exact topic names and suffix-style wildcard patterns such as `location_*`. + +Subscribe-only topics include: +- `location_updates` +- `location_updates:geojson` +- `proximity_updates` +- `trackable_motions` +- `fence_events` +- `fence_events:geojson` +- `collision_events` when collisions are enabled +- `metadata_changes` for resource create, update, and delete notifications on zones, fences, trackables, and location providers + +## Ownership Claims + +Ownership-aware rules use the claim configured by `AUTH_OWNED_RESOURCES_CLAIM`. + +Expected shape: + +```json +{ + "": { + "provider_ids": ["provider-1"], + "trackable_ids": ["trackable-1"], + "zone_ids": ["zone-1"], + "fence_ids": ["fence-1"], + "source_ids": ["source-1"] + } +} +``` + +For `*_OWN` permissions, the request path parameter must be present in the matching owned-resource list. + +## Error Handling + +- `401 Unauthorized`: missing bearer header, malformed token, invalid signature, bad issuer, bad audience, expired token, or other authentication failure +- `403 Forbidden`: authenticated token lacks a matching permission or ownership claim +- `403 Forbidden` on RPC also covers missing method-level discovery or invocation permission +- WebSocket auth failures are returned as OMLOX wrapper `error` events with code `10004` + +Authentication failures return a `WWW-Authenticate: Bearer` header and the API error body. + +## WebSocket Authentication + +When auth is enabled: +- every WebSocket `subscribe` and `message` event must carry the JWT access token in `params.token` +- the hub authenticates and authorizes each message independently +- the WebSocket upgrade itself is intentionally allowed without an HTTP bearer header so the OMLOX `params.token` model can be used +- route-style REST permissions do not grant WebSocket topic access automatically; the `websocket` policy block must allow the topic + +If a topic is valid but disabled by configuration, the WebSocket layer returns an OMLOX wrapper `error` event with code `10002` and a descriptive message instead of treating it as an unknown topic. + +## Dex Development Setup + +This repository includes a Dex fixture at [tools/dex/config.yaml](https://github.com/Open-Location-Stack/open-location-hub/blob/main/tools/dex/config.yaml) and a matching permissions file at [config/auth/permissions.yaml](https://github.com/Open-Location-Stack/open-location-hub/blob/main/config/auth/permissions.yaml). + +`docker compose` starts Dex on port `5556` and configures the app container to verify Dex-issued tokens with: + +- `AUTH_MODE=oidc` +- `AUTH_ISSUER=http://dex:5556/dex` +- `AUTH_AUDIENCE=open-rtls-cli` +- `AUTH_ROLES_CLAIM=email` + +Included test users: + +- `admin@example.com` / `testpass123` +- `reader@example.com` / `testpass123` +- `owner@example.com` / `testpass123` + +Fetch a token: + +```bash +curl -sS -X POST http://localhost:5556/dex/token \ + -u open-rtls-cli:cli-secret \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data 'grant_type=password&scope=openid%20email%20profile&username=admin@example.com&password=testpass123' +``` + +Use the returned `access_token` as the bearer token when calling the hub. + +## Other Providers + +Keycloak and similar OIDC providers fit the same model if they expose: + +- issuer discovery +- JWKS +- a stable audience for the hub +- a claim that can be mapped via `AUTH_ROLES_CLAIM` + +For production deployments, prefer a real role or group claim instead of the Dex development fixture's email-based mapping. The hub is intended to verify JWT access tokens from the production IdP, not development-specific token handling. + +## RPC Security Guidance + +For RPC in production: +- require JWT auth +- treat `GET /v2/rpc/available` as sensitive metadata +- grant `com.omlox.ping` and `com.omlox.identify` more broadly only if operators really need them +- grant `com.omlox.core.xcmd` only to tightly controlled roles or automation identities +- keep MQTT broker access narrow so user-facing applications cannot bypass the hub's policy and audit layer + +## End-to-End Coverage + +The integration suite boots Dex and the hub, obtains a bearer token from Dex, and proves: + +- authenticated requests reach protected endpoints +- missing or invalid tokens return `401` +- insufficient permissions return `403` +- ownership-restricted routes reject tokens that lack owned-resource claims diff --git a/content/open-location-hub/docs/configuration.md b/content/open-location-hub/docs/configuration.md new file mode 100644 index 0000000..8ff44a5 --- /dev/null +++ b/content/open-location-hub/docs/configuration.md @@ -0,0 +1,110 @@ +--- +title: "Configuration" +description: "All runtime configuration is environment-driven." +draft: false +generated: true +generated_from: "docs/configuration.md" +github_url: "https://github.com/Open-Location-Stack/open-location-hub/blob/main/docs/configuration.md" +--- +_This page is generated from the Open Location Hub source documentation and should not be edited in the website repository._ + +All runtime configuration is environment-driven. + +Runtime lifecycle behavior: +- the hub process runs from a single signal-aware root context created from `SIGINT` and `SIGTERM` +- startup failures return structured process errors instead of panicking during early config or logger initialization +- graceful shutdown uses a bounded timeout so HTTP shutdown and internal event-publisher fan-out can complete deterministically after a stop signal + +## Core +- `HTTP_LISTEN_ADDR` (default `:8080`) +- `HTTP_REQUEST_BODY_LIMIT_BYTES` (default `4194304`) +- `LOG_LEVEL` (default `info`) +- `HUB_ID` (optional UUID bootstrap or reset value for the persisted hub identity) +- `HUB_LABEL` (optional bootstrap or reset value for the persisted human-readable hub label) +- `RESET_HUB_ID` (`true`/`false`, default `false`; when `true`, overwrite stored hub metadata with explicitly supplied env values) +- `POSTGRES_URL` (default `postgres://postgres:postgres@localhost:5432/openrtls?sslmode=disable`) +- `MQTT_BROKER_URL` (default `tcp://localhost:1883`) +- `WEBSOCKET_WRITE_TIMEOUT` (duration, default `5s`) +- `WEBSOCKET_OUTBOUND_BUFFER` (default `32`) + +Hub metadata bootstrap behavior: +- the hub persists one durable metadata row in Postgres containing the stable `hub_id` and operator-facing label +- on first startup, `HUB_ID` seeds that row when provided; otherwise the hub generates a UUIDv7 +- on first startup, `HUB_LABEL` seeds that row when provided; otherwise the hub defaults to the machine hostname and falls back to `open-rtls-hub` if hostname lookup is unavailable +- on later startups, the stored row is the source of truth when `HUB_ID` and `HUB_LABEL` are omitted +- if supplied env values disagree with the stored row, startup fails with a clear mismatch error unless `RESET_HUB_ID=true` +- when `RESET_HUB_ID=true`, only explicitly supplied values overwrite the stored row; omitted fields are preserved + +HTTP request decoding behavior: +- JSON request bodies are capped by `HTTP_REQUEST_BODY_LIMIT_BYTES` before decode work proceeds +- the REST/RPC handler layer accepts exactly one JSON document per request body and rejects trailing JSON tokens +- unknown JSON object fields remain allowed so forwards-compatible clients are not rejected solely for extension data + +## Stateful Processing +- `STATE_LOCATION_TTL` (duration, default `10m`) +- `STATE_PROXIMITY_TTL` (duration, default `5m`) +- `STATE_DEDUP_TTL` (duration, default `2m`) +- `METADATA_RECONCILE_INTERVAL` (duration, default `30s`) +- `RPC_TIMEOUT` (duration, default `5s`) +- `RPC_ANNOUNCEMENT_INTERVAL` (duration, default `1m`) +- `RPC_HANDLER_ID` (default `open-rtls-hub`) +- `COLLISIONS_ENABLED` (`true`/`false`, default `false`) +- `COLLISION_STATE_TTL` (duration, default `2m`) +- `COLLISION_COLLIDING_DEBOUNCE` (duration, default `5s`) + +Stateful ingest behavior: +- duplicate location/proximity payloads inside `STATE_DEDUP_TTL` are suppressed in the in-memory processing state before fan-out work +- latest provider-source location state, trackable latest-location state, proximity hysteresis state, fence membership state, and collision pair state are all kept in process memory with the configured expiry semantics +- metadata is loaded from Postgres at startup, updated immediately after successful CRUD writes, and reconciled in the background every `METADATA_RECONCILE_INTERVAL` +- durable hub metadata is also loaded from Postgres at startup before the service begins accepting traffic +- WebSocket delivery uses a per-connection outbound queue capped by `WEBSOCKET_OUTBOUND_BUFFER`; slow subscribers are disconnected instead of backpressuring the ingest path +- the `metadata_changes` WebSocket topic emits lightweight `{id,type,operation,timestamp}` notifications for zone, fence, trackable, and location-provider CRUD or reconcile drift +- when `COLLISIONS_ENABLED=true`, the hub evaluates trackable-versus-trackable collisions from the latest WGS84 motion state and keeps short-lived collision pair state in memory for `COLLISION_STATE_TTL` +- `COLLISION_COLLIDING_DEBOUNCE` limits repeated `colliding` emissions for already-active pairs + +RPC behavior: +- `RPC_TIMEOUT` is the default wait time for request-response style RPC calls when the client does not supply `_timeout` +- `RPC_ANNOUNCEMENT_INTERVAL` controls how often the hub republishes retained MQTT availability announcements for hub-owned methods +- `RPC_HANDLER_ID` is the handler identifier announced for hub-owned RPC methods and the identifier clients may use with `_handler_id` to target the hub directly +- `com.omlox.identify` returns the persisted hub label as `name` plus the stable `hub_id` + +## Proximity Resolution +- `PROXIMITY_RESOLUTION_ENTRY_CONFIDENCE_MIN` (number, default `0`) +- `PROXIMITY_RESOLUTION_EXIT_GRACE_DURATION` (duration, default `15s`) +- `PROXIMITY_RESOLUTION_BOUNDARY_GRACE_DISTANCE` (number, default `2`) +- `PROXIMITY_RESOLUTION_MIN_DWELL_DURATION` (duration, default `5s`) +- `PROXIMITY_RESOLUTION_POSITION_MODE` (default `zone_position`; supported value: `zone_position`) +- `PROXIMITY_RESOLUTION_FALLBACK_RADIUS` (number, default `0`) +- `PROXIMITY_RESOLUTION_STALE_STATE_TTL` (duration, default `10m`) + +Proximity resolution behavior: +- proximity updates are resolved to a zone before the hub emits a derived `Location` +- the first valid proximity observation enters immediately +- the hub keeps the current zone for a short grace period to reduce flapping between nearby zones +- zone-specific overrides may be supplied through `Zone.properties.proximity_resolution` +- `Proximity.properties` is preserved into derived location metadata but does not override configured policy + +## Auth +- `AUTH_ENABLED` (`true`/`false`, default `true`) +- `AUTH_MODE` (`none|oidc|static|hybrid`, default `none`) +- `AUTH_ISSUER` (OIDC issuer URL) +- `AUTH_AUDIENCE` (comma-separated) +- `AUTH_ALLOWED_ALGS` (comma-separated, default `RS256`) +- `AUTH_STATIC_PUBLIC_KEYS` (comma-separated PEM blocks or JWKS URLs) +- `AUTH_CLOCK_SKEW` (duration, default `30s`) +- `AUTH_OIDC_REFRESH_TTL` (duration, default `10m`) +- `AUTH_HTTP_TIMEOUT` (duration, default `5s`) +- `AUTH_PERMISSIONS_FILE` (YAML path, default `config/auth/permissions.yaml`) +- `AUTH_ROLES_CLAIM` (JWT claim used for role extraction, default `groups`) +- `AUTH_OWNED_RESOURCES_CLAIM` (JWT object claim for owned resource IDs; see `docs/auth.md` for format and usage) + +See [docs/auth.md](/open-location-hub/docs/auth/) for the full auth model, Dex setup, and permission file format. + +## RPC Security Defaults + +For production deployments: +- keep `AUTH_ENABLED=true` +- treat `GET /v2/rpc/available` as sensitive because it reveals reachable control functions +- use per-method RPC permissions in the auth registry to control who may invoke which methods +- grant `com.omlox.core.xcmd` only to tightly controlled operator or automation roles +- keep direct MQTT broker access limited to the hub and trusted device/adaptor components diff --git a/content/open-location-hub/docs/rpc.md b/content/open-location-hub/docs/rpc.md new file mode 100644 index 0000000..d9e4af1 --- /dev/null +++ b/content/open-location-hub/docs/rpc.md @@ -0,0 +1,224 @@ +--- +title: "RPC Guide" +description: "RPC is the hub's command and diagnostics interface." +draft: false +generated: true +generated_from: "docs/rpc.md" +github_url: "https://github.com/Open-Location-Stack/open-location-hub/blob/main/docs/rpc.md" +--- +_This page is generated from the Open Location Hub source documentation and should not be edited in the website repository._ + +## What RPC is + +RPC is the hub's command and diagnostics interface. + +Use it when you need to ask the hub or a downstream RTLS device/controller to +do something right now. Examples: +- check whether the control path is alive +- identify a reachable handler +- send an OMLOX core command through the hub + +Do not use RPC for normal CRUD resource management. Use: +- REST CRUD for zones, trackables, providers, and fences +- MQTT/WebSocket streams for ongoing updates and events +- RPC for targeted commands and diagnostics + +## The simple mental model + +Applications call `open-rtls-hub`. They do not call MQTT devices directly. + +Flow: +1. The client calls `GET /v2/rpc/available` to see what methods are reachable. +2. The client calls `PUT /v2/rpc` with a JSON-RPC request. +3. The hub either handles the method locally or forwards it to the right MQTT handler. +4. The hub applies authorization, logging, timeout, and aggregation rules. +5. The hub returns the JSON-RPC result or error to the client. + +This makes the hub the control-plane front door and audit point. + +## Available methods + +The hub exposes these OMLOX-reserved methods locally: +- `com.omlox.ping` +- `com.omlox.identify` +- `com.omlox.core.xcmd` + +The hub also exposes methods announced by external MQTT handlers when those handlers are reachable. + +Use: + +```bash +curl -sS http://localhost:8080/v2/rpc/available \ + -H "Authorization: Bearer $TOKEN" +``` + +The response maps method names to the reachable handler ids known to the hub. + +## Calling methods + +Call `PUT /v2/rpc` with a JSON-RPC 2.0 request body. + +Example: ping the hub + +```bash +curl -sS -X PUT http://localhost:8080/v2/rpc \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "ping-1", + "method": "com.omlox.ping", + "params": { + "_aggregation": "_return_first_success" + } + }' +``` + +Example: identify a specific handler + +```bash +curl -sS -X PUT http://localhost:8080/v2/rpc \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "identify-1", + "method": "com.omlox.identify", + "params": { + "_handler_id": "open-rtls-hub" + } + }' +``` + +Example: send an OMLOX core command through the hub + +```bash +curl -sS -X PUT http://localhost:8080/v2/rpc \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "xcmd-1", + "method": "com.omlox.core.xcmd", + "params": { + "_handler_id": "open-rtls-hub", + "command": "XCMD_REQ", + "payload": { + "example": true + } + } + }' +``` + +Note: +- `com.omlox.core.xcmd` already exists at the hub control-plane layer +- device execution is provided through the adapter configured for the target deployment +- if no adapter is configured, the hub returns a deterministic unsupported JSON-RPC error + +## What the built-in methods mean + +### `com.omlox.ping` + +Use it to prove the RPC path is alive. It returns a small result containing the +hub handler id, the message `pong`, and a timestamp. + +### `com.omlox.identify` + +Use it to learn what the hub exposes. It returns the service name, build +version, auth mode, stable `hub_id`, and built-in method list. + +Hub behavior: +- `name` is the persisted hub label from Postgres-backed hub metadata +- `hub_id` is the stable persisted hub UUID used for internal provenance + +### `com.omlox.core.xcmd` + +Use it when you need to send OMLOX core-zone commands through the hub instead +of opening direct MQTT control access to devices. The hub validates and logs +the call, then routes it through the configured adapter. + +## `_handler_id`, `_timeout`, and `_aggregation` + +### `_handler_id` + +Use `_handler_id` when one specific handler must receive the command. + +Examples: +- target the hub itself with `open-rtls-hub` +- target a specific external handler discovered from `GET /v2/rpc/available` + +### `_timeout` + +Use `_timeout` to override the default wait time in milliseconds for a specific +request. + +### `_aggregation` + +Use `_aggregation` only when the call may have multiple responders. + +Supported values: +- `_all_within_timeout` +- `_return_first_success` +- `_return_first_error` + +Rules: +- `_handler_id` and `_aggregation` cannot be combined +- if `_aggregation` is omitted, the hub defaults to `_all_within_timeout` + +## What happens when multiple handlers answer + +The hub collects responses according to `_aggregation`. + +- `_all_within_timeout`: return one result whose `responses` array contains all responses received before timeout +- `_return_first_success`: return as soon as a non-error response arrives +- `_return_first_error`: return as soon as an error response arrives + +If no suitable response arrives, the hub returns a JSON-RPC error. + +## Authorization + +RPC uses two layers of control: + +1. normal REST bearer-token authentication and route authorization +2. RPC-specific method authorization inside the hub + +That means a caller can be allowed to reach `/v2/rpc` and still be denied for a +specific method. + +Recommended policy shape: +- allow discovery only to trusted operators or automation +- allow `com.omlox.ping` and `com.omlox.identify` to a small support audience if needed +- allow `com.omlox.core.xcmd` only to tightly controlled roles +- deny unknown/custom methods by default until explicitly approved + +## How to secure it + +Plain language rule: + +Do not let every application talk directly to MQTT devices. Let them talk to +the hub. The hub checks identity, checks permissions, logs what was called, and +only then forwards the command. + +Operational guidance: +- require JWT auth for RPC in production +- treat `GET /v2/rpc/available` as sensitive because it shows reachable control functions +- grant invoke permission per method, not just generic RPC access +- restrict `com.omlox.core.xcmd` more tightly than `ping` or `identify` +- keep MQTT broker access narrow to the hub and trusted adapters/devices +- use the hub as the audit and policy boundary +- fail closed when authorization or handler selection is uncertain + +## Logging and audit expectations + +The hub logs: +- the method name +- whether the call was accepted or rejected +- handler selection and timeout/failure paths + +Operators should treat those logs as the primary audit trail for RPC use. + +## Repository behavior + +- the available-method response exposes handler ids for the methods the hub can reach +- the hub publishes retained MQTT availability announcements for hub-owned methods +- `com.omlox.core.xcmd` executes through the deployment adapter configured for the hub and returns a deterministic unsupported error when no adapter is configured diff --git a/i18n/en.yaml b/i18n/en.yaml index dd3e39a..862986e 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -10,6 +10,9 @@ - id: nav_open_location_hub translation: Open Location Hub +- id: nav_open_location_hub_docs + translation: Hub Docs + - id: nav_floor_plan_editor translation: Floor Plan Editor diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index e2f108a..692de0c 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -145,6 +145,7 @@
{{ i18n "nav_home" }} {{ i18n "nav_open_location_hub" }} + {{ i18n "nav_open_location_hub_docs" }} {{ i18n "nav_floor_plan_editor" }} {{ i18n "nav_faq" }} {{ i18n "nav_imprint" }} @@ -167,6 +168,7 @@ {{ i18n "email_link_text" }} {{ i18n "nav_open_location_hub" }} + {{ i18n "nav_open_location_hub_docs" }} {{ i18n "nav_floor_plan_editor" }} {{ i18n "nav_faq" }} {{ i18n "nav_imprint" }} diff --git a/layouts/_default/list.html b/layouts/_default/list.html new file mode 100644 index 0000000..c107e81 --- /dev/null +++ b/layouts/_default/list.html @@ -0,0 +1,51 @@ +{{ define "main" }} +
+
+

{{ .Title }}

+ {{ with .Description }} +

{{ . }}

+ {{ end }} + {{ with .Params.github_url }} + + {{ end }} +
+ +
+ {{ .Content }} +
+ + {{ $sections := .Sections.ByTitle }} + {{ $regularPages := .RegularPages.ByTitle }} + {{ if or (gt (len $sections) 0) (gt (len $regularPages) 0) }} +
+
+ {{ range $sections }} +
+

+ {{ .Title }} +

+ {{ with .Description }} +

{{ . }}

+ {{ end }} +
+ {{ end }} + {{ range $regularPages }} +
+

+ {{ .Title }} +

+ {{ with .Description }} +

{{ . }}

+ {{ end }} +
+ {{ end }} +
+
+ {{ end }} +
+{{ end }}