From 7bda8e6d6049477acb15188af19db82ba8ca343b Mon Sep 17 00:00:00 2001 From: Cam Proudlock Date: Fri, 28 Aug 2026 16:18:34 -0400 Subject: [PATCH] Store the policy JSON columns as TEXT for MySQL 5.6 The branch stored scopejson, desiredstate and params as sa.JSON, the only JSON columns in the product. MySQL 5.6 has no JSON type - it arrived in 5.7 - and the migration failed there with a bare syntax error near the word JSON. They are TEXT now, through a JSONText type decorator, so the seventeen call sites still assign and read dicts. The convention elsewhere is already TEXT: guides.blocksjson, geenforce.manifestjson. Spec regenerated: 331 paths, 477 operations across 22 surfaces. --- docs/openapi.json | 1609 +++++++++++++++++ .../migrations/versions/0004_policies.py | 14 +- plugins/computers/models/jsontext.py | 44 + plugins/computers/models/policy.py | 7 +- 4 files changed, 1668 insertions(+), 6 deletions(-) create mode 100644 plugins/computers/models/jsontext.py diff --git a/docs/openapi.json b/docs/openapi.json index 85ce1fd0..3fc3ff12 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -182,6 +182,9 @@ }, { "name": "guides" + }, + { + "name": "plugin-endpoints" } ], "paths": { @@ -1784,6 +1787,183 @@ ] } }, + "/api/settings/asset-import/script": { + "get": { + "tags": [ + "core-identity" + ], + "summary": "Download Collect-ShopDB.ps1 with this site CSV path already embedded, so the collector script needs no hand editing...", + "description": "Download Collect-ShopDB.ps1 with this site CSV path already embedded, so the collector script needs no hand editing before it runs\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/script -o Collect-ShopDB.ps1\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/settings/asset-import/templates": { + "get": { + "tags": [ + "core-identity" + ], + "summary": "List the importable CSV tables in the order they must be uploaded - foreign keys mean the order is not optional", + "description": "List the importable CSV tables in the order they must be uploaded - foreign keys mean the order is not optional\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/templates\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/settings/asset-import/templates/{tablename}": { + "get": { + "tags": [ + "core-identity" + ], + "summary": "Download a generated CSV template for one importable table, with its real column names as the header row", + "description": "Download a generated CSV template for one importable table, with its real column names as the header row\n\n**Auth:** admin role\n\n**Params:** path: tablename\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/templates/assets -o assets.csv\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": "tablename", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/settings/asset-import/import": { + "post": { + "tags": [ + "core-identity" + ], + "summary": "Validate uploaded CSVs and, only when commit=true, load them. The default is a dry run that reports what WOULD change...", + "description": "Validate uploaded CSVs and, only when commit=true, load them. The default is a dry run that reports what WOULD change; a committing run writes an AuditLog row with the reconciled count\n\n**Auth:** admin role\n\n**Params:** multipart: one file per table; form: commit (false = validate only)\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -F \"assets=@assets.csv\" -F \"commit=true\" http://localhost:5001/api/settings/asset-import/import\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": "multipart: one file per table; form: commit (false = validate only)" + } + } + } + } + } + }, "/api/reports": { "get": { "tags": [ @@ -10057,6 +10237,589 @@ } } }, + "/api/computers/policies/configuration": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "List Configuration Policies (scope + the actions applied to matching endpoints); 404 while the...", + "description": "List Configuration Policies (scope + the actions applied to matching endpoints); 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration\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-computers" + ], + "summary": "Create a Configuration Policy. Scope trees go through validate_scope and every action name is checked against the...", + "description": "Create a Configuration Policy. Scope trees go through validate_scope and every action name is checked against the endpoints command allowlist, so a bad tree or unknown action is rejected before it reaches the database; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.create\n\n**Params:** body: name (required), description, scopejson (scope AST), actions[] of {action, mode: desired|gate, params}\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"name\":\"Shopfloor MTConnect\",\"actions\":[{\"action\":\"mtconnect_start\",\"mode\":\"desired\"}]}' http://localhost:5001/api/computers/policies/configuration\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: name (required), description, scopejson (scope AST), actions[] of {action, mode: desired|gate, params}" + } + } + } + } + } + }, + "/api/computers/policies/configuration/{policy_id}": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "One Configuration Policy with its actions expanded; 404 while the computers_configpolicies_enabled setting is off", + "description": "One Configuration Policy with its actions expanded; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id\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": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "put": { + "tags": [ + "plugin-computers" + ], + "summary": "Update a Configuration Policy; same scope and action validation as create; 404 while the...", + "description": "Update a Configuration Policy; same scope and action validation as create; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.edit\n\n**Params:** path: policy_id; body: any of name, description, scopejson, actions[] (actions replace wholesale)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id\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": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: policy_id; body: any of name, description, scopejson, actions[] (actions replace wholesale)" + } + } + } + } + }, + "delete": { + "tags": [ + "plugin-computers" + ], + "summary": "Delete a Configuration Policy and its actions; 404 while the computers_configpolicies_enabled setting is off", + "description": "Delete a Configuration Policy and its actions; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.delete\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id\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": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/computers/policies/business": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "List Business Policies (ordered bundles of Configuration Policies); 404 while the computers_configpolicies_enabled...", + "description": "List Business Policies (ordered bundles of Configuration Policies); 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business\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-computers" + ], + "summary": "Create a Business Policy. Every referenced Configuration Policy must exist; priority decides which policy wins when two...", + "description": "Create a Business Policy. Every referenced Configuration Policy must exist; priority decides which policy wins when two match the same endpoint; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.create\n\n**Params:** body: name (required), description, priority, items[] of {configurationpolicyid, sortorder}\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business\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: name (required), description, priority, items[] of {configurationpolicyid, sortorder}" + } + } + } + } + } + }, + "/api/computers/policies/business/{policy_id}": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "One Business Policy with its member Configuration Policies in sort order; 404 while the...", + "description": "One Business Policy with its member Configuration Policies in sort order; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id\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": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "put": { + "tags": [ + "plugin-computers" + ], + "summary": "Update a Business Policy or reorder its members; 404 while the computers_configpolicies_enabled setting is off", + "description": "Update a Business Policy or reorder its members; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.edit\n\n**Params:** path: policy_id; body: any of name, description, priority, items[] (items replace wholesale)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id\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": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: policy_id; body: any of name, description, priority, items[] (items replace wholesale)" + } + } + } + } + }, + "delete": { + "tags": [ + "plugin-computers" + ], + "summary": "Delete a Business Policy and its item rows; 404 while the computers_configpolicies_enabled setting is off", + "description": "Delete a Business Policy and its item rows; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.delete\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id\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": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/computers/policies/actions": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "The allowlisted action names and display labels a policy may reference, read from the endpoints command allowlist. The...", + "description": "The allowlisted action names and display labels a policy may reference, read from the endpoints command allowlist. The UI builds its action picker from this rather than hardcoding a list that would drift; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/actions\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/computers/policies/effective/{asset_id}": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "Preview the resolved effective policy for one endpoint: which Business Policy won, which Configuration Policies...", + "description": "Preview the resolved effective policy for one endpoint: which Business Policy won, which Configuration Policies matched, and the resulting desired/gate actions. Read-only, for debugging a scope that is not matching what its author expected; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/effective/1asset_id\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/measuringtools/types": { "get": { "tags": [ @@ -20969,6 +21732,852 @@ } ] } + }, + "/api/endpoints/": { + "get": { + "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```", + "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/{asset_id}": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "One endpoint: its agent state, version, and last check-in", + "description": "One endpoint: its agent state, version, and last check-in\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id\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/{asset_id}/events": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent event history for one endpoint, newest first", + "description": "Agent event history for one endpoint, newest first\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id; query: eventid (page from this id)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id/events\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/asset/{asset_id}/status": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Asset-detail panel: online state, uptime, and agent version for one endpoint", + "description": "Asset-detail panel: online state, uptime, and agent version for one endpoint\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/status\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/asset/{asset_id}/telemetrymeta": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Asset-detail panel: what telemetry this endpoint reports and when each series was last updated", + "description": "Asset-detail panel: what telemetry this endpoint reports and when each series was last updated\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/telemetrymeta\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/asset/{asset_id}/services": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Asset-detail panel: Windows services the agent watches on this endpoint, with running state", + "description": "Asset-detail panel: Windows services the agent watches on this endpoint, with running state\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/services\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/asset/{asset_id}/recentevents": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Asset-detail panel: the most recent agent events for one endpoint", + "description": "Asset-detail panel: the most recent agent events for one endpoint\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/recentevents\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/asset/{asset_id}/interfaces": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Asset-detail panel: network interfaces as the collector reports them ({alias, ipv4, mac, gateway, subnet, dhcp})", + "description": "Asset-detail panel: network interfaces as the collector reports them ({alias, ipv4, mac, gateway, subnet, dhcp})\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/interfaces\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/dashboard/summary": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Dashboard card: endpoint counts by online/offline/never-seen", + "description": "Dashboard card: endpoint counts by online/offline/never-seen\n\n**Auth:** endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/summary\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/dashboard/quiet": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Dashboard card: endpoints that have gone quiet - no check-in inside the configured quiet-hours window", + "description": "Dashboard card: endpoints that have gone quiet - no check-in inside the configured quiet-hours window\n\n**Auth:** endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/quiet\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/dashboard/stability": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Dashboard card: endpoints ranked by reboot/agent-restart churn, to surface a machine that is flapping", + "description": "Dashboard card: endpoints ranked by reboot/agent-restart churn, to surface a machine that is flapping\n\n**Auth:** endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/stability\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/agentkey": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Status of the endpoints agent key: whether one exists, when it was issued, and its last use. Never returns the key...", + "description": "Status of the endpoints agent key: whether one exists, when it was issued, and its last use. Never returns the key itself\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey\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." + } + } + }, + "delete": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Revoke the endpoints agent key. The kill switch for agent ingest: reports and command polls stop being authorized...", + "description": "Revoke the endpoints agent key. The kill switch for agent ingest: reports and command polls stop being authorized immediately\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey\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/agentkey/regenerate": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Issue a new endpoints agent key and return it ONCE. The key is scoped to collector.ingest.endpoints only, so rotating...", + "description": "Issue a new endpoints agent key and return it ONCE. The key is scoped to collector.ingest.endpoints only, so rotating it cannot disturb the computers or GE-Enforce collectors. Every agent must be reconfigured with the new value\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey/regenerate\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": "none" + } + } + } + } + } + }, + "/api/endpoints/{asset_id}/commands": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Command history for one endpoint: what was queued, claimed, and what it returned", + "description": "Command history for one endpoint: what was queued, claimed, and what it returned\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id/commands\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" + } + } + ] + }, + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Queue one allowlisted command for an endpoint. Unknown actions are rejected, destructive ones (system_restart) demand a...", + "description": "Queue one allowlisted command for an endpoint. Unknown actions are rejected, destructive ones (system_restart) demand a stricter permission, the effective policy must permit the action for this endpoint, and a second command will not stack while one is pending or running\n\n**Auth:** endpoints.command\n\n**Params:** path: asset_id; body: action (required, allowlisted), target, targetkind\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"action\":\"service_restart\",\"target\":\"MTConnectAgent\",\"targetkind\":\"service\"}' http://localhost:5001/api/endpoints/1/commands\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" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: asset_id; body: action (required, allowlisted), target, targetkind" + } + } + } + } + } + }, + "/api/endpoints/commands/next": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent long-poll. Claims the oldest pending command for the calling host and atomically marks it running so no second...", + "description": "Agent long-poll. Claims the oldest pending command for the calling host and atomically marks it running so no second poll claims it; 204 when nothing is queued inside the hold window, and the agent re-polls. Collector-token auth, not JWT\n\n**Auth:** collector token (collector.ingest.endpoints)\n\n**Params:** query: hostname (required)\n\n**Example:**\n```\ncurl -H \"X-Collector-Key: $KEY\" \"http://localhost:5001/api/endpoints/commands/next?hostname=WJSF-PC-01\"\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/commands/{command_id}/result": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent reports a claimed command result. Finalizes the command row and, for MTConnect actions that observed run state...", + "description": "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\n\n**Auth:** collector token (collector.ingest.endpoints)\n\n**Params:** path: command_id; body: the command outcome (status, output)\n\n**Example:**\n```\ncurl -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\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": "command_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: command_id; body: the command outcome (status, output)" + } + } + } + } + } } } } \ No newline at end of file diff --git a/plugins/computers/migrations/versions/0004_policies.py b/plugins/computers/migrations/versions/0004_policies.py index d5c7f737..4a78c1cb 100644 --- a/plugins/computers/migrations/versions/0004_policies.py +++ b/plugins/computers/migrations/versions/0004_policies.py @@ -22,6 +22,14 @@ branch_labels = None depends_on = None +# TEXT rather than sa.JSON on purpose. MySQL 5.6 has no JSON type - it arrived +# in 5.7 - and this migration failed there with a bare syntax error near the +# word JSON. Every other JSON-bearing column in the product is TEXT for the same +# reason (guides.blocksjson, geenforce.manifestjson); the model reads and writes +# dicts through the JSONText type decorator, so nothing above the database +# changes. + + def upgrade(): bind = op.get_bind() insp = sa.inspect(bind) @@ -33,7 +41,7 @@ def upgrade(): sa.Column('configurationpolicyid', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=150), nullable=False), sa.Column('description', sa.Text(), nullable=True), - sa.Column('scopejson', sa.JSON(), nullable=True), + sa.Column('scopejson', sa.Text(), nullable=True), sa.Column('source', sa.String(length=50), nullable=False, server_default='local'), sa.Column('createddate', sa.DateTime(), nullable=False), @@ -57,8 +65,8 @@ def upgrade(): sa.Column('action', sa.String(length=100), nullable=False), sa.Column('mode', sa.String(length=20), nullable=False, server_default='gate'), - sa.Column('desiredstate', sa.JSON(), nullable=True), - sa.Column('params', sa.JSON(), nullable=True), + sa.Column('desiredstate', sa.Text(), nullable=True), + sa.Column('params', sa.Text(), nullable=True), sa.Column('intervalminutes', sa.Integer(), nullable=True), sa.Column('sortorder', sa.Integer(), nullable=False, server_default='0'), diff --git a/plugins/computers/models/jsontext.py b/plugins/computers/models/jsontext.py new file mode 100644 index 00000000..82f476e3 --- /dev/null +++ b/plugins/computers/models/jsontext.py @@ -0,0 +1,44 @@ +"""JSON stored in a TEXT column. + +MySQL 5.6 has no JSON type - it arrived in 5.7 - and a site is still running +5.6, where `sa.JSON()` fails at migration time with a bare syntax error near the +word JSON. Every other JSON-bearing column in this product is already TEXT for +that reason: guides' `blocksjson`, GE-Enforce's `manifestjson`. + +This keeps the Python side identical. A model attribute still holds a dict or a +list, still assigns and reads like one; only the storage changes. The +alternative - json.dumps and json.loads at seventeen call sites - is the same +thing written out seventeen times, with seventeen chances to forget one. + +Note the SQLite behaviour is deliberately the same as MySQL's. Tests run on +SQLite and production does not, so a type that behaved differently across the +two would hide exactly the failure this exists to prevent. +""" + +import json + +from sqlalchemy import Text +from sqlalchemy.types import TypeDecorator + + +class JSONText(TypeDecorator): + """A dict or list on the Python side, TEXT in the database.""" + + impl = Text + cache_ok = True + + def process_bind_param(self, value, dialect): + if value is None: + return None + return json.dumps(value, separators=(',', ':')) + + def process_result_value(self, value, dialect): + if value is None or value == '': + return None + try: + return json.loads(value) + except (TypeError, ValueError): + # A row written before this column was JSON, or hand-edited. Hand + # back the raw text rather than raising: a policy that cannot be + # parsed should be visible and fixable, not an exception on read. + return value diff --git a/plugins/computers/models/policy.py b/plugins/computers/models/policy.py index a95ea5a4..1896a9ac 100644 --- a/plugins/computers/models/policy.py +++ b/plugins/computers/models/policy.py @@ -18,6 +18,7 @@ every table for a clean rollback. """ from shopdb.api import db, BaseModel +from .jsontext import JSONText # Provenance of a policy row. @@ -43,7 +44,7 @@ class ConfigurationPolicy(BaseModel): # Boolean target AST: {"type":"and","nodes":[{"type":"pctype","id":3}, # {"type":"not","node":{"type":"app","id":12}}]}. Validated server-side # (known node types, depth cap) before it is stored - see policyscope. - scopejson = db.Column(db.JSON, nullable=True) + scopejson = db.Column(JSONText, nullable=True) # GE-Enforce interop seam (see POLICY_SOURCE_LOCAL). source = db.Column(db.String(50), default=POLICY_SOURCE_LOCAL, nullable=False) @@ -101,11 +102,11 @@ class ConfigurationPolicyAction(BaseModel): # Desired-state target for mode=desired, as JSON so different actions can # express different targets (e.g. {"running": true}). Null for gate mode. - desiredstate = db.Column(db.JSON, nullable=True) + desiredstate = db.Column(JSONText, nullable=True) # Optional action parameters passed through to the command (JSON), and an # optional reconcile cadence in minutes (null = reconcile on every report). - params = db.Column(db.JSON, nullable=True) + params = db.Column(JSONText, nullable=True) intervalminutes = db.Column(db.Integer, nullable=True) sortorder = db.Column(db.Integer, default=0, nullable=False)