Change Control and Approvals
How datasets and geography change in MDS: drafts, submission, approval through a maker-checker permission or AWE, permissions, audit, and what the original write endpoints do now
In MDS, published data is never edited. Every change, to a dataset or to the geography, follows the same path:
Drafts
A dataset has at most one open draft (
DRAFTorSUBMITTED) at a time; so does the geography. A secondcreate_list_draftfails withG2P-CAT-409.A draft starts as a copy of the highest published version (its base). Any edit endpoint creates the draft if none is open, so an explicit
create_list_draftis needed only to set a change note or effective date up front, or to copy an older version's content (copy_from_version).Anyone with the edit permission can change a draft: add, change or retire entries (
upsert_draft_values,retire_draft_values); change the dataset's code, labels, hierarchy flag or attribute schema (update_list); set its change note and effective date (update_list_draft). For geography: levels, units, change events, boundaries, note, effective date and owner.A dataset's
description,owner_org, theme (domain), visibility and licence are administrative:update_listapplies them at once, outside the draft. The geography's visibility and licence are set withupdate_geo_settings.Entries are validated against the dataset's attribute schema as they are written, so errors surface while editing rather than at approval.
Consumers do not see a draft unless they ask for it (
version: "draft"or its number).A draft can be discarded: nothing it contained is published. The version is kept with status
DISCARDED(with its content, and who discarded it indecided_by), so its number is never handed out again. Inpermissionmode a submitted draft can also be discarded; inawemode it cannot (cancel the AWE request instead).
Submit
Submitting freezes the draft (status SUBMITTED) and asks for approval. The submitter may give:
a change note saying what changed and why;
an
effective_from, if the version should take effect later than its approval. It may not be earlier than the latest published version's.
On submit, MDS validates the whole draft again: labels, the entry hierarchy (no missing parents, no cycles), every entry against the attribute schema and every x-list-ref against the referenced dataset's version in effect (on the draft's effective_from, when that is in the future; never a draft: see Typed attributes); for geography, levels, units and every recorded change event. A draft with no changes from its base is refused (G2P-CAT-400). A geography draft also gets its lineage completed: units created, retired, renamed or moved without a recorded event, and units whose boundary geometry changed, get automatic change events (is_auto: true); see Change events.
Approval modes
How a submitted draft is approved is a deployment setting: the chart value masterDataAPI.catalogue.approvalMode (environment variable MASTER_DATA_API_CATALOGUE_APPROVAL_MODE), permission (the default) or awe. get_catalogue_config tells a client which mode is in effect.
permission: maker-checker within MDS
A user holding the publish permission (referenceData:publish for datasets, geo:publish for geography) approves or rejects the draft with approve_draft / reject_draft (approve_geo_draft / reject_geo_draft). A rejection needs a decision_note.
Maker ≠checker is enforced: the approver must not be the user who created the draft, last edited it, or submitted it (G2P-CAT-403). Users are compared by their stable user id: the token's sub claim (if a token has no sub, preferred_username, then name). A display name is never used to decide who may approve: two people can share one, and a person can change theirs. So the *_by fields of a version hold user ids; the display names are kept beside them in *_by_name (and actor_name in the change log) for people to read.
awe: through the Approval Workflow Engine
On submit, MDS opens an approval request in the Approval Workflow Engine, forwarding the submitter's bearer token:
Policy key
awe.policyKeyList (default master_data.list_version.v1)
awe.policyKeyGeo (default master_data.geo_version.v1)
Artifact type
master_data.list_version
master_data.geo_version
Artifact id
<list_id>:<version_no>
geography:<version_no>
Context
list_code, list_id, version_no, owner_org, change_note
version_no, owner_org, country, change_note
The policy is chosen by subject type, not by owner: the owner_org travels in the request's context, where an AWE policy can use it. MDS stores AWE's request id on the version (approval_ref). If AWE cannot be reached, submit fails with G2P-CAT-502 and the draft stays a draft.
When AWE reaches a decision it calls POST /catalogue/awe/callback. This endpoint takes no user token; it is authenticated by an HMAC signature, as in the registry's AWE integration: X-Approval-Signature: sha256=HMAC_SHA256(secret, "<X-Approval-Timestamp>." + body), with X-Approval-Timestamp (within 300 s) and X-Approval-Event-Id. The body is AWE's webhook event (event_id, event_type, request_id, artifact_type, artifact_id, status, stage_order, actor, occurred_at). request_approved publishes the version; request_rejected and request_cancelled mark it REJECTED. The decision is recorded with the actor AWE reports as decided_by (and decided_by_name), or awe when AWE reports none. The callback is idempotent on event_id (received events are kept in g2p_catalogue_awe_events). It answers 401 for a bad signature and 422 for an event that cannot be applied.
The approve_* and reject_* endpoints are disabled in awe mode. If awe is set but no AWE base URL is configured, MDS falls back to permission mode.
When approvalMode is awe, the chart:
mints the callback HMAC secret (
<release>-master-data-awe-callback-hmac), kept across upgrades;with
awe.seed.enabled(default on), runs anawe-seedcontainer in the geo-seed Job that registers both policies in the shared AWE database: one stage, one approval, approvers holding the client roleMASTER_DATA_APPROVERon themaster-dataclient, self-approval forbidden. It also registers the callback secret. The stages can be edited in AWE afterwards.
Releases
Catalogue releases are approved by permission only, in both modes: publish_release needs referenceData:publish and a publisher who is neither the creator of the release nor whoever last set its members (recorded as members_set_by / members_set_at), compared by stable user id (G2P-CAT-403).
Publish
Publishing is not a separate call; it happens on approval. In one transaction MDS:
validates the version once more;
marks it
PUBLISHED, setspublished_atandeffective_from(the approval's, else the draft's, else now);for geography, sets
valid_fromon new units andvalid_toon retired ones, and dates change events without one;if the version is now in effect, refreshes the current published state in the existing tables (
g2p_attributesandg2p_attribute_values, org2p_geo_levelsandg2p_geo_level_values);writes
*.draft.approvedand*.version.publishedentries to the change log.
From then on the database triggers refuse any change to the version. After the transaction commits, MDS delivers the events from the change log (the outbox; see Audit): to the Audit Manager and, if a WebSub hub is configured, a "published" notification. A version published with a future effective_from is announced again when it takes effect (*.version.effective).
Retiring
An entry or unit is retired in a draft and stops being current when that draft is published. It is never deleted. There is no endpoint to retire a whole published version or dataset: a dataset that is no longer used can have all its entries retired in a new version.
Permissions
All permissions are held under the master-data Keycloak client (the chart's global.authClientId) and registered with IAM by the chart.
referenceData:create
Create datasets (create_list; legacy add_attribute, add_attribute_value)
referenceData:edit
Open, edit, submit and discard dataset drafts; update dataset metadata (including theme, visibility, licence); create releases and set their members
referenceData:delete
Retire entries in a draft; delete a draft release; legacy delete_attribute, delete_attribute_value
referenceData:publish
Approve or reject dataset drafts (permission mode); publish releases
geo:create
Legacy add_geo_level, add_geo_level_value only
geo:edit
Open, edit, submit and discard the geography draft: levels, units, change events, boundaries; set the geography's visibility and licence
geo:delete
Retire units in the geography draft; legacy delete_geo_level, delete_geo_level_value
geo:publish
Approve or reject geography drafts (permission mode)
MASTER_DATA_ADMIN
All of the above
MASTER_DATA_APPROVER
referenceData:publish, geo:publish only: approves and publishes, cannot edit
Reads need authentication only, as before. The chart registers the permissions and both roles with IAM (iamRegister) and creates the roles in Keycloak.
Audit
MDS sends two kinds of audit events to the Audit Manager (off when no Audit Manager URL is configured; masterDataAPI.catalogue.auditManagerUrl, by default the commons-services Audit Manager):
Every API call, from an audit middleware like the registry APIs': who called which endpoint and the outcome (source
/openg2p/master-data-api). Authenticated calls are always recorded; anonymous calls only when they fail (for example a 404 or 429 on the public catalogue)./pingand the OpenAPI pages are not recorded. Sending never delays the response.Every catalogue lifecycle step, from the change log, described below.
Every lifecycle step is recorded twice:
in MDS's own change log (
g2p_catalogue_change_log), append-only (a trigger refuses updates and deletes; the one column that may change isforwarded_at), which also backs the change feed;in the Audit Manager, as a CloudEvent (
POST <auditManagerUrl>/v1/auditmanager/events; see Standards). The change log is always kept.
The change log is the outbox. An event is written to the change log in the same transaction as the change, so the log is exact; delivery happens afterwards and is retried until it succeeds:
Right after a request's transaction commits, the API sends that request's events (in the background; the request does not wait).
A relay in the API, run every
catalogue_outbox_relay_seconds(environment variableMASTER_DATA_API_CATALOGUE_OUTBOX_RELAY_SECONDS, 30 by default), sends every event whoseforwarded_atis still empty: events whose first delivery failed, events the country-pack loader wrote, and events the database wrote itself (*.version.effective,*.version.migrated). Only one API worker in the deployment relays at a time (a Postgres advisory lock), and rows are claimed withFOR UPDATE SKIP LOCKED, so the two paths never send the same event at once. The relay sends inevent_idorder and stops at the first event that cannot be delivered, retrying it on the next run.forwarded_atis set once the event has been accepted everywhere it goes, or there was nowhere to send it.
Every event
Yes
—
list.version.published, geo.version.published
Yes
<prefix>.list.published, <prefix>.geo.published
list.version.effective, geo.version.effective
Yes
<prefix>.list.effective, <prefix>.geo.effective
release.published
Yes
<prefix>.release.published
<prefix> is websub_topic_prefix (default openg2p.master-data). Delivery is at least once: a receiver may see an event twice (for example if MDS stopped after sending but before recording it). The CloudEvent id is derived from the change-log event_id, and the WebSub payload carries event_id, so a receiver de-duplicates on it.
The CloudEvent type is org.openg2p.master_data.<event_type>, its subject <subject_type>/<subject_id>/v<version_no>, and its data carries the actor (a user's stable id and display name, or a system actor), the action, the resource (master_data.list, master_data.geo or master_data.release), the change-log event_id and the details. The event types are listed in the API reference.
What the legacy write endpoints do now
The original /attributes and /geo write endpoints keep their paths, payloads and response shapes but no longer change published data. Each one writes into the open draft, creating a draft from the highest published version if there is none, and returns the changed object as before (not the draft). A legacy write to a dataset or geography whose draft is already SUBMITTED fails until that draft is decided or discarded.
/attributes/add_attribute
Created a live dataset
Creates the dataset with an empty draft (version 1); nothing is published
/attributes/update_attribute
Changed a dataset
Changes the dataset's code, label or hierarchy flag in the draft
/attributes/delete_attribute
Deleted a dataset
A dataset never published is deleted. A published dataset is never deleted: all its entries are retired in the draft. Either way, existing entries block it unless cascade is set
/attributes/add_attribute_value
Added a live entry
Adds the entry to the draft
/attributes/update_attribute_value
Changed a live entry
Changes the entry in the draft (children follow a changed code)
/attributes/delete_attribute_value
Deleted a live entry
Retires the entry in the draft (removes it if it was new in the draft); refused while it has active children
/geo/add_geo_level, update_geo_level, delete_geo_level
Changed live levels
Change the levels in the geography draft; a level is removed only if it has no child levels and no units
/geo/add_geo_level_value, update_geo_level_value, delete_geo_level_value
Changed live units
Add, change or retire the unit in the geography draft
Last updated
Was this helpful?