-
Notifications
You must be signed in to change notification settings - Fork 0
Description
📌 Parent Issue
#182 (Phase 3: Secret Sharing & Access Control)
🎯 Goal
Implement secret sharing API to grant/revoke access between users and roles with fine-grained permissions (read/write/admin).
📋 Implementation Tasks
1. SecretShareController
Location: app/Http/Controllers/Api/V1/SecretShareController.php
Endpoints:
-
POST /api/v1/secrets/{secret}/shares- Grant access to user or role- Request body:
{ "user_id": "uuid", // XOR with role_id "role_id": 123, // XOR with user_id "permission": "read", // read|write|admin "expires_at": "2025-12-31T23:59:59Z" // optional } - Validation: Exactly one of user_id OR role_id (XOR constraint)
- Authorization: SecretSharePolicy@create
- Response: Created share with 201 status
- Request body:
-
GET /api/v1/secrets/{secret}/shares- List all shares for secret- Include: User details, role details, permission level, expiration
- Authorization: SecretSharePolicy@viewAny (owner or admin permission)
- Response: Array of shares
-
DELETE /api/v1/secrets/{secret}/shares/{share}- Revoke access- Check: Owner OR granter OR has admin permission
- Authorization: SecretSharePolicy@delete
- Response: 204 No Content
2. SecretSharePolicy
Location: app/Policies/SecretSharePolicy.php
Methods:
-
viewAny(User $user, Secret $secret)- Can list shares (owner or admin) -
create(User $user, Secret $secret)- Can grant access (owner or admin) -
delete(User $user, SecretShare $share)- Can revoke (owner, granter, or admin)
Implementation:
public function create(User $user, Secret $secret): bool
{
return $secret->userHasPermission($user, 'admin');
}
public function delete(User $user, SecretShare $share): bool
{
// Owner always can
if ($share->secret->owner_id === $user->id) {
return true;
}
// Granter can revoke their own grants
if ($share->granted_by === $user->id) {
return true;
}
// Admin permission on secret
return $share->secret->userHasPermission($user, 'admin');
}3. Form Request
ShareSecretRequest:
-
user_id- nullable, exists:users,id, uuid -
role_id- nullable, exists:roles,id -
permission- required, in:read,write,admin -
expires_at- nullable, date, after:now - Custom rule: Exactly one of
user_idORrole_id(XOR validation)
XOR Validation:
public function rules(): array
{
return [
'user_id' => ['nullable', 'uuid', 'exists:users,id'],
'role_id' => ['nullable', 'exists:roles,id'],
'permission' => ['required', 'in:read,write,admin'],
'expires_at' => ['nullable', 'date', 'after:now'],
];
}
public function withValidator($validator): void
{
$validator->after(function ($validator) {
$hasUser = !is_null($this->user_id);
$hasRole = !is_null($this->role_id);
if ($hasUser === $hasRole) {
$validator->errors()->add('user_id',
'Exactly one of user_id or role_id must be provided.'
);
}
});
}4. Routes
Location: routes/api.php
Route::middleware(['auth:sanctum', 'tenant.enforce'])->prefix('v1')->group(function () {
Route::post('secrets/{secret}/shares', [SecretShareController::class, 'store']);
Route::get('secrets/{secret}/shares', [SecretShareController::class, 'index']);
Route::delete('secrets/{secret}/shares/{share}', [SecretShareController::class, 'destroy']);
});5. Tests (Pest)
Feature Tests - SecretShareController:
- Grant access to user (owner)
- Grant access to role (owner)
- Grant access with expiration
- Grant access (unauthorized - not owner/admin)
- List shares (owner)
- List shares (user with admin permission)
- List shares (unauthorized user)
- Revoke access (owner)
- Revoke access (granter)
- Revoke access (user with admin permission)
- Revoke access (unauthorized user)
- Validation: XOR constraint (both user_id + role_id fails)
- Validation: XOR constraint (neither user_id nor role_id fails)
Feature Tests - SecretSharePolicy:
- Owner can create shares
- User with admin permission can create shares
- User with write permission cannot create shares
- Granter can delete their own shares
- Owner can delete any share
- User with admin permission can delete shares
- User without permission cannot delete shares
Feature Tests - Integration:
- User with read share can view secret (via SecretController)
- User with write share can update secret
- User with admin share can delete secret
- Expired share does not grant access
Coverage Target: ≥80% for new code
✅ Acceptance Criteria
- SecretShareController with 3 endpoints
- SecretSharePolicy with 3 authorization methods
- ShareSecretRequest with XOR validation
- Routes registered in api.php
- All tests passing (≥21 tests)
- PHPStan level max passing
- Laravel Pint passing
- REUSE 3.3 compliant (SPDX headers)
- CHANGELOG.md updated (Added section)
- No breaking changes to existing endpoints
🔗 Dependencies
- Depends on:
- ✅ PR feat: add SecretShare model and migration (Phase 3 foundation) #183 (SecretShare model) - Merged
- PHPStan: SecretController mixed type warnings from request->input() #184 (SecretController) - BLOCKED UNTIL THIS IS DONE
- Blocks:
- feat: add user language preference endpoint (#86) #186 (Integration Tests)
- Part of: Issue Phase 3: Secret Sharing & Access Control (RBAC Integration) #182 (Phase 3: Secret Sharing)
📝 Technical Notes
Response Formats
// POST /v1/secrets/{secret}/shares
{
"data": {
"id": "uuid",
"secret_id": "uuid",
"user": {
"id": "uuid",
"name": "John Doe",
"email": "john@example.com"
},
"role": null,
"permission": "read",
"granted_by": {
"id": "uuid",
"name": "Jane Smith"
},
"granted_at": "2025-11-17T10:00:00Z",
"expires_at": "2025-12-31T23:59:59Z"
}
}
// GET /v1/secrets/{secret}/shares
{
"data": [
{
"id": "uuid",
"user": { "id": "uuid", "name": "John Doe" },
"role": null,
"permission": "read",
"granted_by": { "id": "uuid", "name": "Owner" },
"granted_at": "2025-11-17T10:00:00Z",
"expires_at": null,
"is_expired": false
},
{
"id": "uuid",
"user": null,
"role": { "id": 5, "name": "Manager" },
"permission": "write",
"granted_by": { "id": "uuid", "name": "Owner" },
"granted_at": "2025-11-16T15:30:00Z",
"expires_at": "2026-01-01T00:00:00Z",
"is_expired": false
}
]
}Error Handling
// XOR Validation Error
{
"message": "The given data was invalid.",
"errors": {
"user_id": [
"Exactly one of user_id or role_id must be provided."
]
}
}
// Permission Denied
{
"message": "This action is unauthorized."
}📝 PR Linking Instructions
When creating the PR:
Fixes #185
Part of: #182
Depends on: #184- Do NOT start until PHPStan: SecretController mixed type warnings from request->input() #184 is merged
- Do NOT use
Fixes #182- this is not the last sub-issue - Estimated LOC: ~400-450
Type: Sub-Issue (Backend)
Priority: High
Estimated Effort: 3-4 days
Status: ⏸️ Blocked by #184
Metadata
Metadata
Assignees
Type
Projects
Status