From 56ed89bb7b8febbf8a40fe3fca5225c7b86d138e Mon Sep 17 00:00:00 2001 From: cproudlock Date: Wed, 2 Sep 2026 13:49:54 -0400 Subject: [PATCH] Document the ten new endpoint routes and undo the stray re-indent Two of the three things holding the wilm import out of main. The monitor CRUD, the two resolver reads and the three operator-report routes had no entry in docs/api-inventory.json, so test_api_inventory_parity failed. The gate cannot run on the published mirror - the test ships but its data file is scrubbed, and it skips on the file's absence - which is why the branch that added the routes never saw it. Cost lands here at promotion time instead. /api/endpoints is registered both with and without a trailing slash and both forms are served, so both are described rather than guessing which one counts. Spec regenerated: 353 paths, 504 operations. plugins/endpoints/plugin.py had a contiguous run of continuation lines shifted by one space. Python accepted it and git diff -w showed nothing, but it put deletions in a diff that is otherwise purely additive. Restored; the file now differs from the published main by additions only. Scope: product --- docs/api-inventory.json | 84 ++++++- docs/openapi.json | 430 +++++++++++++++++++++++++++++++++++- plugins/endpoints/plugin.py | 26 +-- 3 files changed, 523 insertions(+), 17 deletions(-) diff --git a/docs/api-inventory.json b/docs/api-inventory.json index d649c157..b326b594 100644 --- a/docs/api-inventory.json +++ b/docs/api-inventory.json @@ -3773,10 +3773,10 @@ "endpoints": [ { "method": "GET", - "path": "/api/endpoints", + "path": "/api/endpoints/", "auth": "endpoints.view", "params": "query: search (hostname substring)", - "purpose": "List endpoints reporting through the ShopDB agent, with last-seen and online state", + "purpose": "List endpoint states. Registered both with and without the trailing slash, so both forms are described here", "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints" }, { @@ -3922,6 +3922,86 @@ "params": "path: command_id; body: the command outcome (status, output)", "purpose": "Agent reports a claimed command result. Finalizes the command row and, for MTConnect actions that observed run state, caches that on the EndpointState so the UI toggle reflects reality between polls - both in one commit. Collector-token auth, not JWT", "example": "curl -X POST -H \"X-Collector-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"status\":\"succeeded\",\"output\":\"ok\"}' http://localhost:5001/api/endpoints/commands/1/result" + }, + { + "path": "/api/endpoints/monitors", + "method": "GET", + "auth": "jwt + require_permission endpoints.view", + "params": "none", + "purpose": "List every monitor definition, shipped and local, ordered shipped-first then by display name", + "example": "curl http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\"" + }, + { + "path": "/api/endpoints/monitors", + "method": "POST", + "auth": "jwt + require_role admin", + "params": "body: kind (process|service, required), displayname (required), processname (required for process), servicename (required for service), launchpath, datacollectorpath, logcollectorpath, isactive (default true)", + "purpose": "Create a local monitor definition; source is forced to 'local' so a caller cannot mint a shipped row", + "example": "curl -X POST http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"kind\":\"service\",\"displayname\":\"Spooler\",\"servicename\":\"Spooler\"}'" + }, + { + "path": "/api/endpoints/monitors/", + "method": "PUT", + "auth": "jwt + require_role admin", + "params": "path: monitor_id; body: same fields as create, all optional", + "purpose": "Update a local monitor definition; 403 on a shipped row, which is product-owned and read-only", + "example": "curl -X PUT http://localhost:5001/api/endpoints/monitors/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'" + }, + { + "path": "/api/endpoints/monitors/", + "method": "DELETE", + "auth": "jwt + require_role admin", + "params": "path: monitor_id", + "purpose": "Delete a local monitor definition; 403 on a shipped row", + "example": "curl -X DELETE http://localhost:5001/api/endpoints/monitors/4 -H \"Authorization: Bearer $TOK\"" + }, + { + "path": "/api/endpoints/monitors/resolve", + "method": "GET", + "auth": "jwt + require_permission endpoints.view", + "params": "query: pctype (required)", + "purpose": "Resolve the monitor set a PC type should watch: shipped defaults for 'Shopfloor PC' plus whatever active site policy attaches to that type. The human-facing view of what the agent will be told", + "example": "curl 'http://localhost:5001/api/endpoints/monitors/resolve?pctype=Shopfloor+PC' -H \"Authorization: Bearer $TOK\"" + }, + { + "path": "/api/endpoints/monitors/resolved", + "method": "GET", + "auth": "endpoints collector API key (X-API-Key/Bearer), not JWT", + "params": "query: hostname (required)", + "purpose": "Agent runtime watch-list. The agent calls this each telemetry cycle and receives {processes[], services[]} shaped like its own MonitorConfig, so a policy-attached monitor is watched without editing appsettings.json on the bay. Unknown hostname is a 404; a PC type with no monitors returns empty lists", + "example": "curl 'http://localhost:5001/api/endpoints/monitors/resolved?hostname=F7MHPF24' -H \"X-API-Key: $ENDPOINTS_KEY\"" + }, + { + "path": "/api/endpoints/operatorreports", + "method": "POST", + "auth": "endpoints collector API key (X-API-Key/Bearer), not JWT", + "params": "multipart/form-data: hostname (required), reason (default 'Other (free text)', truncated to 128), details, screenshot (png, 10 MB cap)", + "purpose": "Agent uploads one operator issue report captured from the tray. The hostname must resolve to a computer asset. The screenshot is stored under a server-generated uuid filename; a retention sweep runs opportunistically so the feed stays bounded without a scheduler", + "example": "curl -X POST http://localhost:5001/api/endpoints/operatorreports -H \"X-API-Key: $ENDPOINTS_KEY\" -F hostname=F7MHPF24 -F 'details=Machine will not home' -F screenshot=@shot.png" + }, + { + "path": "/api/endpoints/panel//operatorreports", + "method": "GET", + "auth": "jwt + require_permission endpoints.view", + "params": "path: asset_id", + "purpose": "Operator Reports asset panel: this endpoint's reports newest first, each carrying a hasimage flag so the viewer knows whether to request the screenshot", + "example": "curl http://localhost:5001/api/endpoints/panel/46/operatorreports -H \"Authorization: Bearer $TOK\"" + }, + { + "path": "/api/endpoints/operatorreports//image", + "method": "GET", + "auth": "jwt + require_permission endpoints.view", + "params": "path: operatorreportid", + "purpose": "Serve one operator report's screenshot, hardened with the shared upload headers (CSP sandbox + nosniff); 404 when the report carries no image", + "example": "curl http://localhost:5001/api/endpoints/operatorreports/12/image -H \"Authorization: Bearer $TOK\" -o shot.png" + }, + { + "method": "GET", + "path": "/api/endpoints", + "auth": "endpoints.view", + "params": "query: search (hostname substring)", + "purpose": "List endpoint states. Registered both with and without the trailing slash, so both forms are described here", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints" } ] }, diff --git a/docs/openapi.json b/docs/openapi.json index 3d20663c..180739c7 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -21826,8 +21826,8 @@ "tags": [ "plugin-endpoints" ], - "summary": "List endpoints reporting through the ShopDB agent, with last-seen and online state", - "description": "List endpoints reporting through the ShopDB agent, with last-seen and online state\n\n**Auth:** endpoints.view\n\n**Params:** query: search (hostname substring)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints\n```", + "summary": "List endpoint states. Registered both with and without the trailing slash, so both forms are described here", + "description": "List endpoint states. Registered both with and without the trailing slash, so both forms are described here\n\n**Auth:** endpoints.view\n\n**Params:** query: search (hostname substring)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints\n```", "security": [ { "bearerAuth": [] @@ -22718,6 +22718,432 @@ } } }, + "/api/endpoints/monitors": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "List every monitor definition, shipped and local, ordered shipped-first then by display name", + "description": "List every monitor definition, shipped and local, ordered shipped-first then by display name\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Create a local monitor definition; source is forced to 'local' so a caller cannot mint a shipped row", + "description": "Create a local monitor definition; source is forced to 'local' so a caller cannot mint a shipped row\n\n**Auth:** jwt + require_role admin\n\n**Params:** body: kind (process|service, required), displayname (required), processname (required for process), servicename (required for service), launchpath, datacollectorpath, logcollectorpath, isactive (default true)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"kind\":\"service\",\"displayname\":\"Spooler\",\"servicename\":\"Spooler\"}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: kind (process|service, required), displayname (required), processname (required for process), servicename (required for service), launchpath, datacollectorpath, logcollectorpath, isactive (default true)" + } + } + } + } + } + }, + "/api/endpoints/monitors/{monitor_id}": { + "put": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Update a local monitor definition; 403 on a shipped row, which is product-owned and read-only", + "description": "Update a local monitor definition; 403 on a shipped row, which is product-owned and read-only\n\n**Auth:** jwt + require_role admin\n\n**Params:** path: monitor_id; body: same fields as create, all optional\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/endpoints/monitors/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "monitor_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: monitor_id; body: same fields as create, all optional" + } + } + } + } + }, + "delete": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Delete a local monitor definition; 403 on a shipped row", + "description": "Delete a local monitor definition; 403 on a shipped row\n\n**Auth:** jwt + require_role admin\n\n**Params:** path: monitor_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/endpoints/monitors/4 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "monitor_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/endpoints/monitors/resolve": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Resolve the monitor set a PC type should watch: shipped defaults for 'Shopfloor PC' plus whatever active site policy...", + "description": "Resolve the monitor set a PC type should watch: shipped defaults for 'Shopfloor PC' plus whatever active site policy attaches to that type. The human-facing view of what the agent will be told\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** query: pctype (required)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/endpoints/monitors/resolve?pctype=Shopfloor+PC' -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/monitors/resolved": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent runtime watch-list. The agent calls this each telemetry cycle and receives {processes[], services[]} shaped like...", + "description": "Agent runtime watch-list. The agent calls this each telemetry cycle and receives {processes[], services[]} shaped like its own MonitorConfig, so a policy-attached monitor is watched without editing appsettings.json on the bay. Unknown hostname is a 404; a PC type with no monitors returns empty lists\n\n**Auth:** endpoints collector API key (X-API-Key/Bearer), not JWT\n\n**Params:** query: hostname (required)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/endpoints/monitors/resolved?hostname=F7MHPF24' -H \"X-API-Key: $ENDPOINTS_KEY\"\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/operatorreports": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent uploads one operator issue report captured from the tray. The hostname must resolve to a computer asset. The...", + "description": "Agent uploads one operator issue report captured from the tray. The hostname must resolve to a computer asset. The screenshot is stored under a server-generated uuid filename; a retention sweep runs opportunistically so the feed stays bounded without a scheduler\n\n**Auth:** endpoints collector API key (X-API-Key/Bearer), not JWT\n\n**Params:** multipart/form-data: hostname (required), reason (default 'Other (free text)', truncated to 128), details, screenshot (png, 10 MB cap)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/operatorreports -H \"X-API-Key: $ENDPOINTS_KEY\" -F hostname=F7MHPF24 -F 'details=Machine will not home' -F screenshot=@shot.png\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "multipart/form-data: hostname (required), reason (default 'Other (free text)', truncated to 128), details, screenshot (png, 10 MB cap)" + } + } + } + } + } + }, + "/api/endpoints/panel/{asset_id}/operatorreports": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Operator Reports asset panel: this endpoint's reports newest first, each carrying a hasimage flag so the viewer knows...", + "description": "Operator Reports asset panel: this endpoint's reports newest first, each carrying a hasimage flag so the viewer knows whether to request the screenshot\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/panel/46/operatorreports -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/endpoints/operatorreports/{operatorreportid}/image": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Serve one operator report's screenshot, hardened with the shared upload headers (CSP sandbox + nosniff); 404 when the...", + "description": "Serve one operator report's screenshot, hardened with the shared upload headers (CSP sandbox + nosniff); 404 when the report carries no image\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** path: operatorreportid\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/operatorreports/12/image -H \"Authorization: Bearer $TOK\" -o shot.png\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "operatorreportid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, "/api/scripts": { "get": { "tags": [ diff --git a/plugins/endpoints/plugin.py b/plugins/endpoints/plugin.py index f35182ab..21eac0af 100644 --- a/plugins/endpoints/plugin.py +++ b/plugins/endpoints/plugin.py @@ -167,19 +167,19 @@ class EndpointsPlugin(BasePlugin): # running a staged script or pushing an installer is a strictly # heavier grant than a routine service/process toggle. ('endpoints.deploy', - 'Run staged scripts and push application installers to endpoints', 'endpoints'), - # Packaging the agent is a heavier grant than deploying to one, and - # it is not the same job either: the generated ZIP carries a live - # collector key stamped into its install script, so whoever holds - # this can mint a distributable artefact containing the fleet-wide - # command-channel credential. It was require_role('admin'), which - # this codebase otherwise reserves for the few genuinely - # role-shaped checks - and a role cannot be granted to one person - # for one job the way a permission can. - ('endpoints.agentpackage', - 'Generate and download an agent or adoption-tool package ' - '(contains a live collector key)', 'endpoints'), - # Collector service scope for the endpoint agent. A managed token + 'Run staged scripts and push application installers to endpoints', 'endpoints'), + # Packaging the agent is a heavier grant than deploying to one, and + # it is not the same job either: the generated ZIP carries a live + # collector key stamped into its install script, so whoever holds + # this can mint a distributable artefact containing the fleet-wide + # command-channel credential. It was require_role('admin'), which + # this codebase otherwise reserves for the few genuinely + # role-shaped checks - and a role cannot be granted to one person + # for one job the way a permission can. + ('endpoints.agentpackage', + 'Generate and download an agent or adoption-tool package ' + '(contains a live collector key)', 'endpoints'), + # Collector service scope for the endpoint agent. A managed token # scoped to ONLY this authorizes the endpoints collector and nothing # else, so rotating/revoking it stops endpoint-agent traffic without # touching computers/geenforce collectors. Minted from the settings