|
| 1 | +# Admin REST API Migration Guide |
| 2 | + |
| 3 | +The admin REST API response shapes have changed as part of the authorization system refactoring. All endpoints use the same URL paths and HTTP methods. Request bodies are unchanged. Only response JSON shapes differ. |
| 4 | + |
| 5 | +## Unchanged endpoints |
| 6 | + |
| 7 | +These endpoints have no response changes: |
| 8 | + |
| 9 | +| Endpoint | Description | |
| 10 | +|----------|-------------| |
| 11 | +| `POST /tenants/:id` | Update tenant name | |
| 12 | +| `POST /tenants/:id/groups` | Create group under tenant | |
| 13 | +| `POST /groups/:id` | Update group name | |
| 14 | +| `POST /tenants/:id/permissions` | Add permission to tenant | |
| 15 | +| `DELETE /tenants/:id/permissions` | Remove permission from tenant | |
| 16 | +| `POST /groups/:id/permissions` | Add permission to group | |
| 17 | +| `DELETE /groups/:id/permissions` | Remove permission from group | |
| 18 | +| `POST /groups/:id/tenant` | Set group's parent tenant | |
| 19 | +| `POST /feeds/:id/group` | Set feed's parent group | |
| 20 | +| `POST /feed_versions/:id/permissions` | Add permission to feed version | |
| 21 | +| `DELETE /feed_versions/:id/permissions` | Remove permission from feed version | |
| 22 | +| `GET /users` | List users | |
| 23 | +| `GET /users/:id` | Get user | |
| 24 | + |
| 25 | +## Changed endpoints |
| 26 | + |
| 27 | +### `GET /me` |
| 28 | + |
| 29 | +```jsonc |
| 30 | +// Before |
| 31 | +{ |
| 32 | + "user": {"id": "ian", "name": "Ian", "email": "ian@example.com"}, |
| 33 | + "groups": [{"id": 1, "name": "BA-group"}], |
| 34 | + "expanded_groups": [{"id": 1, "name": "BA-group"}, {"id": 2, "name": "CT-group"}], |
| 35 | + "external_data": {"gatekeeper": "..."}, |
| 36 | + "roles": ["admin"] |
| 37 | +} |
| 38 | + |
| 39 | +// After |
| 40 | +{ |
| 41 | + "id": "ian", |
| 42 | + "name": "Ian", |
| 43 | + "email": "ian@example.com", |
| 44 | + "groups": [{"id": 1, "name": "BA-group"}], |
| 45 | + "expanded_groups": [{"id": 1, "name": "BA-group"}, {"id": 2, "name": "CT-group"}], |
| 46 | + "external_data": {"gatekeeper": "..."}, |
| 47 | + "roles": ["admin"] |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +**Changes:** User fields (`id`, `name`, `email`) are at the top level instead of nested under `user`. |
| 52 | + |
| 53 | +### `GET /tenants`, `GET /groups`, `GET /feeds`, `GET /feed_versions` |
| 54 | + |
| 55 | +All list endpoints now return a flat array of `ObjectRef` instead of a type-specific wrapper. |
| 56 | + |
| 57 | +```jsonc |
| 58 | +// Before (GET /tenants) |
| 59 | +{ |
| 60 | + "tenants": [ |
| 61 | + {"id": 1, "name": "tl-tenant"}, |
| 62 | + {"id": 2, "name": "other-tenant"} |
| 63 | + ] |
| 64 | +} |
| 65 | + |
| 66 | +// After (GET /tenants) |
| 67 | +[ |
| 68 | + {"type": "tenant", "id": 1}, |
| 69 | + {"type": "tenant", "id": 2} |
| 70 | +] |
| 71 | +``` |
| 72 | + |
| 73 | +```jsonc |
| 74 | +// Before (GET /feeds) |
| 75 | +{ |
| 76 | + "feeds": [ |
| 77 | + {"id": 5, "onestop_id": "BA", "name": "BART"}, |
| 78 | + {"id": 6, "onestop_id": "CT", "name": "Caltrain"} |
| 79 | + ] |
| 80 | +} |
| 81 | + |
| 82 | +// After (GET /feeds) |
| 83 | +[ |
| 84 | + {"type": "feed", "id": 5}, |
| 85 | + {"type": "feed", "id": 6} |
| 86 | +] |
| 87 | +``` |
| 88 | + |
| 89 | +**Changes:** |
| 90 | +- Response is a JSON array, not an object with a type-specific key |
| 91 | +- Each item is `{"type": "...", "id": N}` instead of a full entity object |
| 92 | +- Entity names and other fields (e.g., `onestop_id`) are no longer included in list responses |
| 93 | +- Use the permissions endpoint (`GET /:type/:id`) to get full details for a specific entity |
| 94 | + |
| 95 | +### `GET /tenants/:id`, `GET /groups/:id`, `GET /feeds/:id`, `GET /feed_versions/:id` |
| 96 | + |
| 97 | +All permissions endpoints now return a generic `ObjectPermissions` shape instead of type-specific responses. |
| 98 | + |
| 99 | +```jsonc |
| 100 | +// Before (GET /tenants/1) |
| 101 | +{ |
| 102 | + "tenant": {"id": 1, "name": "tl-tenant"}, |
| 103 | + "groups": [{"id": 2, "name": "BA-group"}], |
| 104 | + "actions": { |
| 105 | + "can_view": true, |
| 106 | + "can_edit": true, |
| 107 | + "can_edit_members": true, |
| 108 | + "can_create_org": true, |
| 109 | + "can_delete_org": true |
| 110 | + }, |
| 111 | + "users": { |
| 112 | + "admins": [{"type": "user", "id": "ian", "name": "Ian", "relation": "admin"}], |
| 113 | + "members": [{"type": "user", "id": "drew", "name": "Drew", "relation": "member"}] |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +// After (GET /tenants/1) |
| 118 | +{ |
| 119 | + "ref": {"type": "tenant", "id": 1, "name": "tl-tenant"}, |
| 120 | + "actions": { |
| 121 | + "can_view": true, |
| 122 | + "can_edit": true, |
| 123 | + "can_edit_members": true, |
| 124 | + "can_create_org": true, |
| 125 | + "can_delete_org": true |
| 126 | + }, |
| 127 | + "subjects": [ |
| 128 | + {"subject": {"type": "user", "name": "ian"}, "relation": "admin", "name": "Ian"}, |
| 129 | + {"subject": {"type": "user", "name": "drew"}, "relation": "member", "name": "Drew"} |
| 130 | + ], |
| 131 | + "children": [ |
| 132 | + {"type": "org", "id": 2, "name": "BA-group"} |
| 133 | + ] |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +```jsonc |
| 138 | +// Before (GET /groups/2) |
| 139 | +{ |
| 140 | + "group": {"id": 2, "name": "BA-group"}, |
| 141 | + "tenant": {"id": 1, "name": "tl-tenant"}, |
| 142 | + "feeds": [{"id": 5, "onestop_id": "BA", "name": "BART"}], |
| 143 | + "actions": {"can_view": true, ...}, |
| 144 | + "users": { |
| 145 | + "managers": [...], |
| 146 | + "editors": [...], |
| 147 | + "viewers": [...] |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +// After (GET /groups/2) |
| 152 | +{ |
| 153 | + "ref": {"type": "org", "id": 2, "name": "BA-group"}, |
| 154 | + "actions": {"can_view": true, ...}, |
| 155 | + "subjects": [ |
| 156 | + {"subject": {"type": "user", "name": "ian"}, "relation": "viewer", "name": "Ian"} |
| 157 | + ], |
| 158 | + "parent": {"type": "tenant", "id": 1, "name": "tl-tenant"}, |
| 159 | + "children": [ |
| 160 | + {"type": "feed", "id": 5, "name": "BART"} |
| 161 | + ] |
| 162 | +} |
| 163 | +``` |
| 164 | + |
| 165 | +**Changes:** |
| 166 | + |
| 167 | +| Before | After | |
| 168 | +|--------|-------| |
| 169 | +| Entity at top level (`tenant`, `group`, `feed`, `feed_version`) | `ref` with `{type, id, name}` | |
| 170 | +| Parent as type-specific field (`tenant` on groups, `group` on feeds) | `parent` with `{type, id, name}` | |
| 171 | +| Children as type-specific field (`groups` on tenants, `feeds` on groups) | `children` array of `{type, id, name}` | |
| 172 | +| Users grouped by role (`admins`, `members`, `managers`, `editors`, `viewers`) | `subjects` flat array with `.relation` field | |
| 173 | +| Actions include `false` values via `omitempty` (absent = false) | Actions only include `true` values (absent = false) | |
| 174 | +| Feed-specific fields in children (`onestop_id`) | Only `name` available on children | |
| 175 | + |
| 176 | +### Key differences summary |
| 177 | + |
| 178 | +1. **Generic shape** — all entity types return the same JSON structure |
| 179 | +2. **`subjects` replaces role-grouped users** — filter by `.relation` client-side instead of accessing `.users.admins`, `.users.editors`, etc. |
| 180 | +3. **`parent`/`children` replace type-specific fields** — `parent` is always a single `ObjectRef`, `children` is always an array |
| 181 | +4. **Actions only include granted permissions** — denied actions are absent from the map, not present as `false` |
| 182 | +5. **List endpoints return bare `ObjectRef` arrays** — no entity details; use the detail endpoint for names |
0 commit comments