feat(drive): prepare E2E tests on drive membership
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
# =============================================================
|
||||
# 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
|
||||
@@ -152,6 +152,7 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test
|
||||
"$API_DIR/groups_effective_members.hurl" \
|
||||
"$API_DIR/grants_nested_groups.hurl" \
|
||||
"$API_DIR/drives_foundation.hurl" \
|
||||
"$API_DIR/drives_membership.hurl" \
|
||||
"$API_DIR/external_users.hurl" \
|
||||
"$API_DIR/search_basic.hurl" \
|
||||
"$API_DIR/nc_second_user_setup.hurl" \
|
||||
|
||||
Reference in New Issue
Block a user