fix(contact): use Permission::Create for creations

This commit is contained in:
Edouard Vanbelle
2026-07-17 00:37:45 +02:00
parent 38abe6766c
commit eb884f6c8f
2 changed files with 61 additions and 6 deletions
+15 -6
View File
@@ -535,10 +535,16 @@ impl ContactUseCase for ContactService {
let address_book_id = Uuid::parse_str(&dto.address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
// Check if user has write access to the address book
// AuthZ audit #19 (2026-07-12): previously required
// `Permission::Update`, which is NOT in the Contributor bundle
// (Read + Create) — Contributor grantees on a shared address
// book couldn't add contacts via REST or CardDAV PUT despite
// holding the intended Create permission. `Delete` uses Delete
// (audit #13, above); creation must use Create. Same fix
// applied to `create_contact_from_vcard` + `create_group`.
let caller_id = Uuid::parse_str(&dto.user_id)
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?;
self.require_address_book_perm(&address_book_id, &caller_id, Permission::Update)
self.require_address_book_perm(&address_book_id, &caller_id, Permission::Create)
.await?;
// Convert DTOs to domain entities
@@ -614,10 +620,13 @@ impl ContactUseCase for ContactService {
let address_book_id = Uuid::parse_str(&dto.address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
// Check if user has write access to the address book
// AuthZ audit #19 — see the sibling `create_contact` above.
// This is the CardDAV `PUT contact.vcf` entry point; the fix
// unblocks Contributor grantees creating contacts through the
// CardDAV protocol as well as the REST surface.
let caller_id = Uuid::parse_str(&dto.user_id)
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?;
self.require_address_book_perm(&address_book_id, &caller_id, Permission::Update)
self.require_address_book_perm(&address_book_id, &caller_id, Permission::Create)
.await?;
// Parse vCard data
@@ -889,10 +898,10 @@ impl ContactUseCase for ContactService {
let address_book_id = Uuid::parse_str(&dto.address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
// Check if user has write access to the address book
// AuthZ audit #19 — see the sibling `create_contact` above.
let caller_id = Uuid::parse_str(&dto.user_id)
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?;
self.require_address_book_perm(&address_book_id, &caller_id, Permission::Update)
self.require_address_book_perm(&address_book_id, &caller_id, Permission::Create)
.await?;
let group = ContactGroup::new(address_book_id, dto.name);
+46
View File
@@ -518,6 +518,52 @@ HTTP 200
jsonpath "$.id" == "{{audit13_contact_id}}"
# ─────────────────────────────────────────────────────────────
# Step 21h–21i — Regression pin for AuthZ audit #19 (2026-07-12).
#
# `ContactService::create_contact` + `create_contact_from_vcard`
# + `create_group` used to `authz.require(Update)` on the address
# book, which the Contributor bundle (Read + Create) does NOT
# satisfy — so Contributor grantees were blocked from adding
# contacts via REST or CardDAV PUT despite holding the intended
# Create permission. Not a bypass, an over-restrictive gate.
# Fix: `Permission::Create`. Sibling `#13` above closed the
# mirror bug on the delete verbs.
#
# The pin demotes Bob from Editor (Step 21d) to Contributor —
# Contributor is the minimal role that MUST succeed post-fix and
# FAILED pre-fix. Bob then POSTs a contact via REST; pre-fix this
# 403'd, post-fix returns 201.
# ─────────────────────────────────────────────────────────────
# 21h — Demote Bob from Editor to Contributor.
PUT {{base_url}}/api/grants/role
Authorization: Bearer {{token}}
Content-Type: application/json
{
"subject": { "type": "user", "id": "{{bob_user_id}}" },
"resource": { "type": "address_book", "id": "{{share_book_id}}" },
"role": "contributor"
}
HTTP 200
# 21i — Bob (Contributor) creates a contact → 201. Pre-fix, the
# service required Update which Contributor's bundle doesn't hold,
# so this 403'd and the CardDAV surface was equally blocked.
POST {{base_url}}/api/address-books/{{share_book_id}}/contacts
Authorization: Bearer {{bob_token}}
Content-Type: application/json
{
"full_name": "audit-19 contributor-can-create canary"
}
HTTP 201
[Captures]
audit19_contact_id: jsonpath "$.id"
# Step 22 — Alice revokes the grant.
DELETE {{base_url}}/api/grants/{{share_grant_id}}
Authorization: Bearer {{token}}