Skip to content

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, zerotouchdeploymentemail/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":{}}'
response
403 {"code":"OEM_DEFAULT_READONLY", "...": "..."}
# 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\"}}"
response
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.