---
module: fleet.task-permissions
title: Task Permissions Guide
tab: Testing
order: 35
audience: internal
---

How to verify the gate actually holds. Run against **bcos.dev** — never prod.

## ⚠️ These Tests Write
`PUT /fleet/task-permissions/{key}` replaces an institution's grants for real. On bcos.dev, `C1BD` normally has `["REBOOT"]`. Record the starting state and restore it, or use `C2BD`/`C3BD` as scratch.

```bash
curl -s https://api.bcos.dev/fleet/task-permissions -H "Authorization: Bearer $ADMIN"   # record this first
```

## Tokens
```bash
login() { curl -s https://api.bcos.dev/auth/api/login -H 'Content-Type: application/json' \
  -d "{\"email\":\"$1\",\"password\":\"Passw0rd-d3v!\"}" \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])'; }

ADMIN=$(login bcos_a@bcos.dev)                  # BCOS_ADMIN
MSP=$(login msp_a@msp1.bcos.dev)                # MSP_ADMIN, scope C1BD+C2BD
CUST=$(login customer_a@customer1.bcos.dev)     # CUSTOMER, C1BD
```

## 1. Role Gate on the Admin Endpoints
A CUSTOMER must be refused. The sidebar hiding the tab is cosmetic — this is the real control.

```bash
for p in fleet/task-permissions fleet/task-permissions/grantable; do
  curl -s -o /dev/null -w "$p %{http_code}\n" "https://api.bcos.dev/$p" -H "Authorization: Bearer $CUST"
done
```
**Expect 403 on both.** MSP_ADMIN and BCOS_ADMIN get 200.

## 2. `/mine` Is Open By Design
```bash
curl -s https://api.bcos.dev/fleet/task-permissions/mine -H "Authorization: Bearer $CUST"
```
**Expect 200** returning only that customer's own grants. This is intentional, not a hole — it lets the UI hide commands the caller would 403 on. A 403 here is the bug.

## 3. Deny By Default
```bash
curl -s -X PUT https://api.bcos.dev/fleet/task-permissions/C3BD \
  -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' -d '[]'
curl -s -X POST https://api.bcos.dev/fleet/tasks \
  -H "Authorization: Bearer $(login customer_a@customer3.bcos.dev)" -H 'Content-Type: application/json' \
  -d '{"taskKind":"REBOOT","atmIds":["C3-ATM-001"]}' -o /dev/null -w '%{http_code}\n'
```
**Expect 403**, message *"Your institution is not permitted to push 'REBOOT'…"*.

## 4. Grant, Then It Works
```bash
curl -s -X PUT https://api.bcos.dev/fleet/task-permissions/C3BD \
  -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' -d '["REBOOT"]'
```
Re-run the POST from step 3 → **expect 200/201**. This actually reboots a simulator; that is the point of a dev simulator.

## 5. The Allowlist Cannot Be Widened
```bash
curl -s -X PUT https://api.bcos.dev/fleet/task-permissions/C3BD \
  -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' \
  -d '["REBOOT","CONFIG_UPDATE"]'
```
**Expect 400**, naming `CONFIG_UPDATE` and listing the grantable set. A 200 here is a **critical** finding — the allowlist is the security boundary.

## 6. Non-Grantable Is Structurally Refused
Even with everything ticked, a customer pushing `CONFIG_UPDATE` gets **403** *"'CONFIG_UPDATE' can never be pushed by a customer."* — a different message from the un-granted case. Both are 403; the messages are the assertion.

## 7. Admins Are Not Gated
With `C3BD` set to `[]`, push `REBOOT` as `$ADMIN` → **expect success**. Admins are ungoverned by this table; a 403 here means the gate is over-reaching.

## 8. Replace, Not Merge
```bash
curl -s -X PUT .../C3BD -d '["REBOOT","RESTART_AGENT"]' ...   # both
curl -s -X PUT .../C3BD -d '["RESTART_AGENT"]' ...            # REBOOT should be GONE
curl -s https://api.bcos.dev/fleet/task-permissions -H "Authorization: Bearer $ADMIN"
```
**Expect `C3BD: ["RESTART_AGENT"]`.** PUT takes complete desired state, not a delta.

## 🔄 Restore
```bash
curl -s -X PUT https://api.bcos.dev/fleet/task-permissions/C1BD \
  -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' -d '["REBOOT"]'
```
Then diff the full map against what you recorded at the top.

## UI Checks
The SPA has no login page and no auth interceptor — the HiveIQ Browser injects the token at session level. In a plain browser `/users/me` 401s, `userRole` stays null, and the admin-only tab is filtered out of the sidebar. Test the UI **in the HiveIQ Browser**, or the tab will appear "missing" for reasons that have nothing to do with permissions.
