219 lines
11 KiB
Plaintext
219 lines
11 KiB
Plaintext
# =============================================================
|
|
# OxiCloud — D2 drive membership API + delegation + caller_role
|
|
# =============================================================
|
|
# Verifies the D2 membership surface on personal drives (the only
|
|
# drive kind today — shared-drive positive cases land alongside D3's
|
|
# create endpoint):
|
|
#
|
|
# 1. `GET /api/drives` exposes `caller_role` on every row.
|
|
# 2. `GET /api/drives/{id}/members` lists role grants on a drive
|
|
# (one Owner row for the lifecycle-hook-provisioned default).
|
|
# 3. Personal-drive guard refuses every membership mutation
|
|
# (POST / PATCH / DELETE on `/api/drives/{id}/members*`) with
|
|
# 405 — personal drives are single-user single-owner.
|
|
# 4. The same guard fires on the generic `/api/grants` write paths
|
|
# (POST / PUT / DELETE) when `resource.type='drive'` and the
|
|
# drive is personal — verifies the `DriveManagementService`
|
|
# delegation that closes the bypass.
|
|
# 5. Drive grants surface in `GET /api/grants/incoming/resources`
|
|
# with `resource_types=drive` filter (previously hard-skipped).
|
|
# 6. Anti-enum: an unrelated user gets the same `404` for a drive
|
|
# they can't read, whether or not it exists.
|
|
#
|
|
# Self-contained: provisions its own users so it can run after
|
|
# drives_foundation without aliasing state.
|
|
# =============================================================
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 1 — admin login
|
|
# ─────────────────────────────────────────────────────────────
|
|
POST {{base_url}}/api/auth/login
|
|
Content-Type: application/json
|
|
{ "username": "{{username}}", "password": "{{password}}" }
|
|
|
|
HTTP 200
|
|
[Captures]
|
|
admin_token: jsonpath "$.access_token"
|
|
admin_user_id: jsonpath "$.user.id"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 2 — Admin's GET /api/drives now includes `caller_role`.
|
|
# Personal-drive owner role is seeded by
|
|
# PersonalDriveLifecycleHook on user creation.
|
|
# ─────────────────────────────────────────────────────────────
|
|
GET {{base_url}}/api/drives
|
|
Authorization: Bearer {{admin_token}}
|
|
|
|
HTTP 200
|
|
[Asserts]
|
|
jsonpath "$[0].kind" == "personal"
|
|
jsonpath "$[0].default_for_user" == "{{admin_user_id}}"
|
|
jsonpath "$[0].caller_role" == "owner"
|
|
|
|
[Captures]
|
|
admin_drive_id: jsonpath "$[0].id"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 3 — GET /api/drives/{id}/members returns the lifecycle-
|
|
# seeded Owner row and only that.
|
|
# ─────────────────────────────────────────────────────────────
|
|
GET {{base_url}}/api/drives/{{admin_drive_id}}/members
|
|
Authorization: Bearer {{admin_token}}
|
|
|
|
HTTP 200
|
|
[Asserts]
|
|
jsonpath "$" count == 1
|
|
jsonpath "$[0].subject.type" == "user"
|
|
jsonpath "$[0].subject.id" == "{{admin_user_id}}"
|
|
jsonpath "$[0].resource.type" == "drive"
|
|
jsonpath "$[0].resource.id" == "{{admin_drive_id}}"
|
|
jsonpath "$[0].role" == "owner"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 4 — Create a fresh user (mbr_alice) so we have a second
|
|
# subject the personal-drive guard can refuse on.
|
|
# ─────────────────────────────────────────────────────────────
|
|
POST {{base_url}}/api/admin/users
|
|
Authorization: Bearer {{admin_token}}
|
|
Content-Type: application/json
|
|
{ "username": "mbr_alice", "password": "MbrAlicePassword1!", "email": "mbr_alice@example.com", "role": "user" }
|
|
|
|
HTTP 201
|
|
[Captures]
|
|
alice_user_id: jsonpath "$.id"
|
|
|
|
POST {{base_url}}/api/auth/login
|
|
Content-Type: application/json
|
|
{ "username": "mbr_alice", "password": "MbrAlicePassword1!" }
|
|
|
|
HTTP 200
|
|
[Captures]
|
|
alice_token: jsonpath "$.access_token"
|
|
|
|
# Alice's GET /api/drives surfaces her own default with caller_role=owner.
|
|
GET {{base_url}}/api/drives
|
|
Authorization: Bearer {{alice_token}}
|
|
|
|
HTTP 200
|
|
[Asserts]
|
|
jsonpath "$" count == 1
|
|
jsonpath "$[0].caller_role" == "owner"
|
|
jsonpath "$[0].default_for_user" == "{{alice_user_id}}"
|
|
|
|
[Captures]
|
|
alice_drive_id: jsonpath "$[0].id"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 5 — Personal-drive guard via the dedicated endpoint:
|
|
# POST /api/drives/{id}/members refuses with 405.
|
|
# ─────────────────────────────────────────────────────────────
|
|
POST {{base_url}}/api/drives/{{admin_drive_id}}/members
|
|
Authorization: Bearer {{admin_token}}
|
|
Content-Type: application/json
|
|
{
|
|
"subject": { "type": "user", "id": "{{alice_user_id}}" },
|
|
"role": "editor"
|
|
}
|
|
|
|
HTTP 405
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 6 — PATCH on a personal drive's members → 405 (even for
|
|
# the owner row itself; personal drives' membership
|
|
# is structurally immutable).
|
|
# ─────────────────────────────────────────────────────────────
|
|
PATCH {{base_url}}/api/drives/{{admin_drive_id}}/members/user/{{admin_user_id}}
|
|
Authorization: Bearer {{admin_token}}
|
|
Content-Type: application/json
|
|
{ "role": "editor" }
|
|
|
|
HTTP 405
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 7 — DELETE on a personal drive's owner → 405. (Verifies
|
|
# the guard fires BEFORE the last-owner check; the order
|
|
# matters for the right error.)
|
|
# ─────────────────────────────────────────────────────────────
|
|
DELETE {{base_url}}/api/drives/{{admin_drive_id}}/members/user/{{admin_user_id}}
|
|
Authorization: Bearer {{admin_token}}
|
|
|
|
HTTP 405
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 8 — Generic /api/grants delegation. POST /api/grants with
|
|
# resource.type='drive' on a personal drive must hit the
|
|
# same guard, otherwise the membership rules are bypassable.
|
|
# ─────────────────────────────────────────────────────────────
|
|
POST {{base_url}}/api/grants
|
|
Authorization: Bearer {{admin_token}}
|
|
Content-Type: application/json
|
|
{
|
|
"resource": { "type": "drive", "id": "{{admin_drive_id}}" },
|
|
"subject": { "type": "user", "id": "{{alice_user_id}}" },
|
|
"role": "editor"
|
|
}
|
|
|
|
HTTP 405
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 9 — PUT /api/grants/role (silent admin update) on a drive
|
|
# resource — also delegated. Same guard fires.
|
|
# ─────────────────────────────────────────────────────────────
|
|
PUT {{base_url}}/api/grants/role
|
|
Authorization: Bearer {{admin_token}}
|
|
Content-Type: application/json
|
|
{
|
|
"resource": { "type": "drive", "id": "{{admin_drive_id}}" },
|
|
"subject": { "type": "user", "id": "{{admin_user_id}}" },
|
|
"role": "editor"
|
|
}
|
|
|
|
HTTP 405
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 10 — Drive grants in /api/grants/incoming/resources.
|
|
# Default UI calls pass `resource_types=file,folder` so
|
|
# drives don't appear; explicit `resource_types=drive`
|
|
# must surface the admin's Owner grant on their own drive.
|
|
# ─────────────────────────────────────────────────────────────
|
|
GET {{base_url}}/api/grants/incoming/resources?resource_types=drive
|
|
Authorization: Bearer {{admin_token}}
|
|
|
|
HTTP 200
|
|
[Asserts]
|
|
jsonpath "$.items" count >= 1
|
|
jsonpath "$.items[0].resource_type" == "drive"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 11 — Anti-enum: Alice queries the members of admin's
|
|
# personal drive. She has no Read on it → 404, same
|
|
# shape as "drive doesn't exist". Operators see the
|
|
# real reason in the audit log; she sees nothing.
|
|
# ─────────────────────────────────────────────────────────────
|
|
GET {{base_url}}/api/drives/{{admin_drive_id}}/members
|
|
Authorization: Bearer {{alice_token}}
|
|
|
|
HTTP 404
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 12 — The same anti-enum shape for a UUID that doesn't
|
|
# exist at all. Indistinguishable from step 11 to the
|
|
# caller — the canonical no-leak response.
|
|
# ─────────────────────────────────────────────────────────────
|
|
GET {{base_url}}/api/drives/00000000-0000-0000-0000-000000000000/members
|
|
Authorization: Bearer {{alice_token}}
|
|
|
|
HTTP 404
|