fix(contact): use Permission::Create for creations
This commit is contained in:
@@ -535,10 +535,16 @@ impl ContactUseCase for ContactService {
|
|||||||
let address_book_id = Uuid::parse_str(&dto.address_book_id)
|
let address_book_id = Uuid::parse_str(&dto.address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.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)
|
let caller_id = Uuid::parse_str(&dto.user_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?;
|
.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?;
|
.await?;
|
||||||
|
|
||||||
// Convert DTOs to domain entities
|
// Convert DTOs to domain entities
|
||||||
@@ -614,10 +620,13 @@ impl ContactUseCase for ContactService {
|
|||||||
let address_book_id = Uuid::parse_str(&dto.address_book_id)
|
let address_book_id = Uuid::parse_str(&dto.address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.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)
|
let caller_id = Uuid::parse_str(&dto.user_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?;
|
.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?;
|
.await?;
|
||||||
|
|
||||||
// Parse vCard data
|
// Parse vCard data
|
||||||
@@ -889,10 +898,10 @@ impl ContactUseCase for ContactService {
|
|||||||
let address_book_id = Uuid::parse_str(&dto.address_book_id)
|
let address_book_id = Uuid::parse_str(&dto.address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.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)
|
let caller_id = Uuid::parse_str(&dto.user_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?;
|
.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?;
|
.await?;
|
||||||
|
|
||||||
let group = ContactGroup::new(address_book_id, dto.name);
|
let group = ContactGroup::new(address_book_id, dto.name);
|
||||||
|
|||||||
@@ -518,6 +518,52 @@ HTTP 200
|
|||||||
jsonpath "$.id" == "{{audit13_contact_id}}"
|
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.
|
# Step 22 — Alice revokes the grant.
|
||||||
DELETE {{base_url}}/api/grants/{{share_grant_id}}
|
DELETE {{base_url}}/api/grants/{{share_grant_id}}
|
||||||
Authorization: Bearer {{token}}
|
Authorization: Bearer {{token}}
|
||||||
|
|||||||
Reference in New Issue
Block a user