refactor(role): use grant only
- remove permission centric mode
- finalize migration drop all tables with permissions
- ensure roles are ENUM (owner is always displayed first)
This commit is contained in:
+22
-19
@@ -122,11 +122,11 @@ Content-Type: application/json
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
# PR N1: POST /api/grants now wraps results in
|
||||
# `CreateGrantResponseDto { grants, notification }`.
|
||||
# Cleanup PR: one role row per (subject, resource). `CreateGrantResponseDto`
|
||||
# wraps a single role-keyed Grant in `.grants[0]`.
|
||||
[Asserts]
|
||||
jsonpath "$.grants" count == 1
|
||||
jsonpath "$.grants[0].permission" == "read"
|
||||
jsonpath "$.grants[0].role" == "viewer"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -148,12 +148,13 @@ Authorization: Bearer {{dave_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$[?(@.resource.id=='{{shared_folder_id}}')].permission" == "read"
|
||||
jsonpath "$[?(@.resource.id=='{{shared_folder_id}}')].role" == "viewer"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 9 — Promote Bob to Admin (adds comment, create, update, share, delete).
|
||||
# PUT /api/grants/role reconciles the row set in one call.
|
||||
# Step 9 — Promote Bob to Owner (covers comment, create, update, share,
|
||||
# delete, manage). PUT /api/grants/role replaces the role in one
|
||||
# UPSERT against `storage.role_grants`.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/api/grants/role
|
||||
Authorization: Bearer {{alice_token}}
|
||||
@@ -161,16 +162,17 @@ Content-Type: application/json
|
||||
{
|
||||
"subject": { "type": "user", "id": "{{dave_user_id}}" },
|
||||
"resource": { "type": "folder", "id": "{{shared_folder_id}}" },
|
||||
"role": "admin"
|
||||
"role": "owner"
|
||||
}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$" count == 6
|
||||
jsonpath "$" count == 1
|
||||
jsonpath "$[0].role" == "owner"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 10 — Bob can now rename (Manager includes update).
|
||||
# Step 10 — Bob can now rename (Owner includes update).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/api/folders/{{shared_folder_id}}/rename
|
||||
Authorization: Bearer {{dave_token}}
|
||||
@@ -194,7 +196,7 @@ HTTP 200
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 12 — Bob re-shares to Carol (he has Share via Admin).
|
||||
# Step 12 — Bob re-shares to Carol (he has Share via Owner).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/grants
|
||||
Authorization: Bearer {{dave_token}}
|
||||
@@ -218,7 +220,7 @@ Authorization: Bearer {{eve_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$[?(@.resource.id=='{{shared_folder_id}}')].permission" == "read"
|
||||
jsonpath "$[?(@.resource.id=='{{shared_folder_id}}')].role" == "viewer"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -247,7 +249,7 @@ Content-Type: application/json
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$" count == 1
|
||||
jsonpath "$[0].permission" == "read"
|
||||
jsonpath "$[0].role" == "viewer"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -263,8 +265,9 @@ HTTP 404
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 17 — Lifecycle: Alice deletes the folder. The DB trigger
|
||||
# trg_cleanup_grants_folder removes both bob's and carol's
|
||||
# grants automatically (also for the cascade-deleted child).
|
||||
# trg_cleanup_role_grants_folder removes both bob's and
|
||||
# carol's grants automatically (also for the cascade-deleted
|
||||
# child).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DELETE {{base_url}}/api/folders/{{child_folder_id}}
|
||||
Authorization: Bearer {{alice_token}}
|
||||
@@ -767,7 +770,7 @@ HTTP 404
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Phase 2D — Promote adam to Admin (all 6 permissions). Delete OK.
|
||||
# Phase 2D — Promote adam to Owner (full bundle, incl. delete). Delete OK.
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
PUT {{base_url}}/api/grants/role
|
||||
Authorization: Bearer {{alice_token}}
|
||||
@@ -775,7 +778,7 @@ Content-Type: application/json
|
||||
{
|
||||
"subject": { "type": "user", "id": "{{adam_user_id}}" },
|
||||
"resource": { "type": "folder", "id": "{{perm_folder_id}}" },
|
||||
"role": "admin"
|
||||
"role": "owner"
|
||||
}
|
||||
|
||||
HTTP 200
|
||||
@@ -788,7 +791,7 @@ HTTP 204
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Phase 2E — Lifecycle cleanup. Alice (still the owner) trashes &
|
||||
# empties; the trigger removes all access_grants rows.
|
||||
# empties; the trigger removes all role_grants rows.
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
DELETE {{base_url}}/api/folders/{{perm_folder_id}}
|
||||
Authorization: Bearer {{alice_token}}
|
||||
@@ -1176,12 +1179,12 @@ Content-Type: application/json
|
||||
{
|
||||
"subject": { "type": "user", "id": "{{frank_user_id}}" },
|
||||
"resource": { "type": "folder", "id": "{{batch_root_id}}" },
|
||||
"role": "admin"
|
||||
"role": "owner"
|
||||
}
|
||||
|
||||
HTTP 200
|
||||
|
||||
# Frank (Admin grant = Delete) trashes batch_file_2 — item goes to
|
||||
# Frank (Owner role includes Delete) trashes batch_file_2 — item goes to
|
||||
# Alice's trash because file.user_id is unchanged (Alice is still owner).
|
||||
POST {{base_url}}/api/batch/trash
|
||||
Authorization: Bearer {{frank_token}}
|
||||
|
||||
@@ -285,7 +285,7 @@ HTTP 201
|
||||
# `CreateGrantResponseDto { grants, notification }`.
|
||||
[Asserts]
|
||||
jsonpath "$.grants" count == 1
|
||||
jsonpath "$.grants[0].permission" == "read"
|
||||
jsonpath "$.grants[0].role" == "viewer"
|
||||
jsonpath "$.grants[0].subject.type" == "group"
|
||||
jsonpath "$.grants[0].subject.id" == "{{group_a_id}}"
|
||||
|
||||
@@ -355,7 +355,7 @@ Authorization: Bearer {{henry_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$[?(@.resource.id=='{{perm_folder_id}}')].permission" == "read"
|
||||
jsonpath "$[?(@.resource.id=='{{perm_folder_id}}')].role" == "viewer"
|
||||
jsonpath "$[?(@.resource.id=='{{perm_folder_id}}')].subject.type" == "group"
|
||||
jsonpath "$[?(@.resource.id=='{{perm_folder_id}}')].subject.id" == "{{group_a_id}}"
|
||||
|
||||
@@ -533,7 +533,7 @@ HTTP 404
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# Phase D — Promote group A's grant to Admin (all 6 permissions).
|
||||
# Phase D — Promote group A's grant to Owner (full bundle).
|
||||
# Delete now succeeds for henry, still flowing through B → A.
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
PUT {{base_url}}/api/grants/role
|
||||
@@ -542,7 +542,7 @@ Content-Type: application/json
|
||||
{
|
||||
"subject": { "type": "group", "id": "{{group_a_id}}" },
|
||||
"resource": { "type": "folder", "id": "{{perm_folder_id}}" },
|
||||
"role": "admin"
|
||||
"role": "owner"
|
||||
}
|
||||
|
||||
HTTP 200
|
||||
|
||||
+30
-23
@@ -4,17 +4,15 @@
|
||||
# Pins the D-Prep refactor behaviours that don't fit naturally into
|
||||
# the existing `grants.hurl` (which is API-shape-focused). Specifically:
|
||||
#
|
||||
# 1. New wire-format role names:
|
||||
# 1. Wire-format role names:
|
||||
# - "owner" is accepted on POST and emitted on response
|
||||
# - "admin" still accepted on POST for one release (compat shim
|
||||
# in `Role::parse`); the server normalises it to Owner
|
||||
# - "admin" is REJECTED with 422 (compat alias retired in the
|
||||
# cleanup PR — see Step 6a)
|
||||
#
|
||||
# 2. Dual-write proof: granting a role and then exercising a
|
||||
# 2. Role-keyed write proof: granting a role and then exercising a
|
||||
# permission from its bundle works → proves the row landed in
|
||||
# `storage.role_grants` because the engine now reads from there
|
||||
# for authz decisions (see `folder_cascade_grant_exists` post-
|
||||
# D-Prep). If dual-write failed, the engine would see no row and
|
||||
# reject the check.
|
||||
# `storage.role_grants` and the engine read path expands the
|
||||
# bundle correctly (see `folder_cascade_grant_exists`).
|
||||
#
|
||||
# 3. Atomic role updates via PUT /api/grants/role — the role flips
|
||||
# in a single SQL update (no DELETE+INSERT race window).
|
||||
@@ -177,10 +175,9 @@ HTTP 204
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 6 — Legacy "admin" string compat. The server's Role::parse
|
||||
# accepts "admin" and normalises to Owner during the D-Prep
|
||||
# dual-write window (one release). Verify by granting sam
|
||||
# with role="admin" — sam should then be able to Delete too.
|
||||
# Step 6a — Reject the legacy "admin" string. The cleanup PR
|
||||
# retired the `#[serde(alias = "admin")]` compat shim on
|
||||
# `RoleDto::Owner`; the deserialiser now refuses it with 422.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/grants
|
||||
Authorization: Bearer {{admin_token}}
|
||||
@@ -191,15 +188,26 @@ Content-Type: application/json
|
||||
"role": "admin"
|
||||
}
|
||||
|
||||
HTTP 422
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 6b — Grant sam Owner with the canonical role string.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/grants
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"subject": { "type": "user", "id": "{{sam_user_id}}" },
|
||||
"resource": { "type": "folder", "id": "{{test_folder_id}}" },
|
||||
"role": "owner"
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
# Capture the first per-permission grant id from the response so
|
||||
# Step 10's revoke doesn't have to query the My Shares endpoint
|
||||
# with awkward JSONPath filtering. Any single grant_id works:
|
||||
# the revoke handler calls `clear_role` which wipes the entire
|
||||
# role_grants row for (sam, test_folder), so the engine reads
|
||||
# return false for sam afterwards regardless of how many
|
||||
# access_grants rows still exist.
|
||||
# The single role-keyed Grant returned in `.grants[0]` is the
|
||||
# `storage.role_grants` row id. Step 10's revoke uses it to
|
||||
# `clear_role` and wipe the row.
|
||||
sam_grant_id: jsonpath "$.grants[0].id"
|
||||
|
||||
|
||||
@@ -326,9 +334,8 @@ body not contains "\"role\":\"admin\""
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 10 — Revoke: removing sam's grant. The handler clears the
|
||||
# access_grants rows AND calls clear_role to wipe the
|
||||
# role_grants row in the same flow.
|
||||
# Step 10 — Revoke: removing sam's grant. `engine.revoke()` DELETEs
|
||||
# the single `storage.role_grants` row by id.
|
||||
#
|
||||
# After: sam's Delete attempt should be refused (proof
|
||||
# the role_grants row is gone — the cascade query for
|
||||
@@ -336,7 +343,7 @@ body not contains "\"role\":\"admin\""
|
||||
# at this folder).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
# sam_grant_id was captured at Step 6 from the create response.
|
||||
# sam_grant_id was captured at Step 6b from the create response.
|
||||
DELETE {{base_url}}/api/grants/{{sam_grant_id}}
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ Content-Type: application/json
|
||||
{
|
||||
"subject": { "type": "group", "id": "{{engineers_id}}" },
|
||||
"resource": { "type": "folder", "id": "{{shared_folder_id}}" },
|
||||
"permissions": ["read"]
|
||||
"role": "viewer"
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
|
||||
Reference in New Issue
Block a user