UNS Governance
This plugin enforces Unified Namespace topic structure at ACL check time.
Plugin API
Base path: /api/v5/plugin_api/emqx_unsgov
Bootstrap Models
- On startup, UNS Governance scans
priv/bootstrap_models/*.json. - For each bootstrap model:
- If its
idis not found in database, plugin stores it and marks it active. - If its
idalready exists in database, plugin skips loading and logs at info level.
- If its
- Bundled default bootstrap model:
priv/bootstrap_models/model-v1.json.
NOTE: Bootstrap models are loaded into the database when the first plugin starts up in the cluster, later plugin or node restarts will not cause a reload. Use API to update the models store in DB.
JSON Data Endpoints
GET /status— plugin status (on_mismatch, exempt_topics).GET /stats— cluster-aggregated counters and recent drops.GET /models— list all stored models (each entry includes anactiveflag).GET /models/:id— get a specific model by ID. 404 if not found.POST /models— create or update a model; optionalactivateflag.POST /models/:id/activate— activate a stored model.POST /models/:id/deactivate— deactivate a model.DELETE /models/:id— delete a stored model.POST /validate/topic— validate a topic against active models.
Other Endpoints
GET /ui— interactive model editor UI.GET /metrics— Prometheus text exposition format.
UNS Model Schema
This section defines the complete model JSON format accepted by UNS Governance.
Top-Level Keys
id(required, string): model ID. Must match^[A-Za-z0-9_-]+$. Controls evaluation order (alphabetical by ID).name(optional, string): model display name. Defaults toid.variable_types(optional, object): reusable variable constraints.tree(required, object): topic tree definition.payload_types(optional, object): reusable payload schemas.
variable_types
Map of variable type name to constraint object.
Supported forms:
- String regex matcher:
{"type":"string","pattern":"^...$"}
- Enum matcher:
{"type":"enum","values":["A","B","C"]}
If a variable type is missing or invalid, matcher falls back to permissive any.
payload_types
Map of payload schema name to schema object.
Validation uses JSON Schema, with one compatibility patch:
- If top-level
typeis omitted, UNS Governance patches it to"object". - Top-level payload schema must be object-root. Primitive roots are rejected.
This allows both:
- Full self-contained object JSON Schema.
- Existing shorthand object schema (for example only
required/properties).
Endpoint payload binding:
- Endpoint
_payloadcan reference a key inpayload_types, or"any"to skip payload validation.
tree
tree is an object where each key is a root topic segment and each value is a node object.
Node object keys:
children(optional, object): child segment map._payload(optional, string): payload type name for endpoint node, default"any"._type(optional, compatibility): explicitnamespace | variable | endpoint._var_type(optional, compatibility): variable type name.
Node type inference:
- If
childrenexists: node is non-endpoint. - If
childrenis absent: node is endpoint. - For non-endpoint keys:
- key
{name}=> variable node - key
+=> variable wildcard node - any other key => namespace node
- key
Variable type resolution:
- For key
{name}:- use
_var_typeif provided - otherwise use inferred type name
name
- use
- For key
+:- matcher is
any(matches one segment)
- matcher is
Wildcard keys in tree:
+: match exactly one topic segment.#: match remaining topic segments (including zero remaining segments).
Full Example
{
"id": "model-v1",
"name": "UNS Model V1",
"variable_types": {
"site_id": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$" },
"line_id": { "type": "string", "pattern": "^Line[0-9]{1,4}$" },
"mode": { "type": "enum", "values": ["auto", "manual"] }
},
"payload_types": {
"line_control": {
"type": "object",
"required": ["Status", "Mode"],
"properties": {
"Status": { "type": "string", "enum": ["running", "stopped"] },
"Mode": { "type": "string", "enum": ["auto", "manual"] }
},
"additionalProperties": false
}
},
"tree": {
"default": {
"children": {
"{site_id}": {
"children": {
"Lines": {
"children": {
"{line_id}": {
"children": {
"LineControl": { "_payload": "line_control" }
}
}
}
},
"stream": {
"children": {
"#": { "_payload": "any" }
}
}
}
}
}
}
}
}Enforcement Behavior
UNS Governance validates both topic structure and (optionally) payload schema.
Topic violations (
topic_nomatch,topic_invalid,not_endpoint):topic_nomatch: no active model topic filter matched the topic. (No model-specific validation is run.) If there are no active models and UNS Governance is enabled, topics are fail-closed astopic_nomatch(exceptexempt_topics).topic_invalid: selected model filter matched, but topic failed selected model structure/segment constraints.not_endpoint: selected model matched topic path, but target node is not an endpoint.- QoS 0: message is ignored.
- QoS 1/2: publish is rejected and a protocol reason code is returned to client (
Not Authorized). - If EMQX
authorization.deny_actionis set todisconnect, the client is disconnected on topic authorization failure (the setting isdisconnect, notdrop). - If
authorization.deny_actionisignore(default), no disconnect is done; QoS 1/2 still receive reject reason codes. - Observable counters:
messages_dropped,topic_nomatch,topic_invalid,not_endpoint, and per-model counters inper_model.
Payload violations (
payload_invalid):- Message is dropped by UNS Governance in publish processing.
- No auth reject/disconnect is required for this path.
- Observable counters:
messages_dropped,payload_invalid, and per-model counters inper_model.
Topic-Filter Pre-Check
When multiple models are active, UNS Governance pre-screens models before full validation:
- Each model is compiled into topic-filter patterns derived from its tree paths.
- Variable segments are converted to single-level wildcards (
+).- Example:
foo/{bar}/xbecomesfoo/+/x.
- Example:
- Active models are ordered by model ID.
- UNS Governance selects the first model (by ID order) whose compiled filter matches the publish topic.
- Pre-check uses direct topic/filter matching only; it does not implicitly expand a publish topic prefix (for example appending
/#). - Only that selected model is fully validated; UNS Governance does not continue to the next model.
- Models that fail this pre-check are skipped and do not contribute per-model drop counters.
This avoids unrelated active models inflating counters and keeps model behavior deterministic. It also means overlapping topic trees across models should be avoided.
Counters
GET /stats returns cluster-aggregated counters.
Top-level counters:
messages_total: total handled messages (messages_allowed + messages_dropped); exempt traffic is included.messages_allowed: allowed messages plus exempt messages.messages_dropped: dropped/rejected messages due to UNS validation failures.topic_nomatch: dropped/rejected because no active model filter matched.topic_invalid: dropped/rejected due to selected-model topic mismatch.not_endpoint: dropped/rejected because topic matched a non-endpoint node.payload_invalid: dropped due to payload schema mismatch.exempt: messages skipped byexempt_topics.per_model: per-model breakdown map keyed by model ID.recent_drops: recent drop events (topic,error_type,error_detail,timestamp_ms).
Per-model counters (per_model.<model_id>):
messages_totalmessages_allowedmessages_droppedtopic_invalidnot_endpointpayload_invalid
Counter semantics:
record_allowedbumpsmessages_totalandmessages_allowedfor the matched model.- Topic/payload drops bump
messages_total,messages_dropped, and the specific reason counter for the selected model. - If no model passes the topic-filter pre-check,
topic_nomatchis bumped globally and no per-model drop counter is bumped. This includes the case where the active model set is empty.