3. Editing a template¶
Done through the Device-Order-Service management API with an org-admin token — the UI cannot do this for every tier.
| Tier | Writable today? |
|---|---|
oem_default |
No, for anyone. OEM_DEFAULT_READONLY (WLPC-86) — there is no platform-operator identity in this service to authorize the write, so it is refused for every caller rather than allowed for any one tenant to reach a platform-wide default. |
tenant_default |
Yes, the caller's own organization (this route checks TEMPLATE_WRITE), server-derived tenant, never client-supplied. |
device_override |
Yes, for a device already in the caller's tenant. |
3.1 Get a token (wlpc-cc-tooling-account)¶
The account is a Keycloak user (email + password, Secret
wlpc-cc-tooling-account, zerotouchdeployment — email/password keys),
not a confidential client — so this is a direct grant (Resource Owner
Password Credentials), not client-credentials. The realm's client
(wlpc-frontend) is itself confidential and needs its own secret (Secret
wl-frontend-oidc, key WLPC_OIDC_CLIENT_SECRET) via HTTP Basic auth on the
token request.
EMAIL=$(kubectl --context=pallax-dev -n zerotouchdeployment get secret wlpc-cc-tooling-account \
-o jsonpath='{.data.email}' | base64 -d)
PASS=$(kubectl --context=pallax-dev -n zerotouchdeployment get secret wlpc-cc-tooling-account \
-o jsonpath='{.data.password}' | base64 -d)
CLIENT_SECRET=$(kubectl --context=pallax-dev -n zerotouchdeployment get secret wl-frontend-oidc \
-o jsonpath='{.data.WLPC_OIDC_CLIENT_SECRET}' | base64 -d)
TOKEN=$(curl -s https://auth.ztd.dev.pxdc.io/realms/wlpc/protocol/openid-connect/token \
-u "wlpc-frontend:${CLIENT_SECRET}" \
--data-urlencode grant_type=password \
--data-urlencode username="${EMAIL}" \
--data-urlencode password="${PASS}" \
--data-urlencode scope="openid profile email" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
Use --data-urlencode, never a hand-built query string
A password containing ~ ! ' ( ) * is form-urlencode-safe as-is but is
not what encodeURIComponent/naive string concatenation would
produce; a hand-assembled body silently sends the wrong bytes for
exactly those characters — the same class of bug that defeated a
leak-scanner elsewhere in this project. Never log $TOKEN,
$CLIENT_SECRET, $PASS; they exist only as shell variables in this
session.
What to do when it fails
A 401 with invalid_grant at the token endpoint almost always means
the password was rotated and this Secret is stale — check
wlpc-cc-tooling-account's age/rotation history before assuming the
account itself is broken.
3.2 Read, preview, write, read back — verified live¶
AUTH="Authorization: Bearer $TOKEN"
# 1. OEM default is refused for everyone:
curl -s -X PUT "$DOS/templates/$(uuidgen)" -H "$AUTH" -H 'content-type: application/json' \
-d '{"tier":"oem_default","body":{}}'
# 2. dry_run previews the blast radius without saving:
curl -s -X PUT "$DOS/templates/$TID?dry_run=true" -H "$AUTH" -H 'content-type: application/json' \
-d "{\"tier\":\"device_override\",\"device_id\":\"$DEVICE_ID\",\"body\":{\"note\":\"preview\"}}"
200 {"would_save":false,"tier":"device_override","affected_device_ids":["..."],"affected_device_count":1}
# 3. Real write:
curl -s -X PUT "$DOS/templates/$TID" -H "$AUTH" -H 'content-type: application/json' \
-d "{\"tier\":\"device_override\",\"device_id\":\"$DEVICE_ID\",\"body\":{\"note\":\"runbook proof edit\",\"mqtt_qos\":2}}"
# -> 200, version:1, body echoes what was sent
# 4. Read it back:
curl -s "$DOS/templates/$TID" -H "$AUTH"
# -> identical to the PUT response
All four ran against a throwaway device created and deleted for this proof (§4) — the real gateway's own template was never touched.
What to do when it fails
TIER_IMMUTABLE (409) means you PUT an existing template id with a
different tier than it already has — tiers never change in place;
SCOPE_TAKEN (409) means the tenant/device already has one of that
tier (there is at most one tenant_default per tenant, one
device_override per device) — GET /templates?... to find the
existing one and PUT that id instead of a new one.