Skip to main content

Create Grant

API Endpoint: POST /admin/resources/{type}/{id}/access-grants

Priority: P1

User Story: As an admin, I want to grant a user access to a resource at a specific access level.

Overview

Create a new access grant for a user on a specific resource. Grants define who can access a resource and at what level (READ, WRITE, ADMIN). Supports optional expiration for temporary access.

Scenarios

Scenario 1: Grant READ access to case

Given:

  • Admin is authenticated with scope access-grants:write
  • Case case_abc123 exists
  • User user_12345 exists
  • User does not have access to case

When:

  • Admin POSTs to /admin/resources/case/case_abc123/access-grants with payload:
    {
    "userId": "user_12345",
    "accessLevel": "READ"
    }

Then:

  • Response status is 201 Created
  • Response body contains:
    {
    "id": "grant_xyz789",
    "userId": "user_12345",
    "resourceType": "case",
    "resourceId": "case_abc123",
    "accessLevel": "READ",
    "grantedBy": "admin_789",
    "grantedAt": "2025-10-19T10:00:00Z",
    "expiresAt": null
    }
  • User can now read the case

Scenario 2: Grant ADMIN access with expiration

Given:

  • Admin is authenticated with scope access-grants:write
  • Document exists
  • User exists

When:

  • Admin POSTs with expiration:
    {
    "userId": "user_67890",
    "accessLevel": "ADMIN",
    "expiresAt": "2025-12-31T23:59:59Z"
    }

Then:

  • Response status is 201 Created
  • Grant is created with expiration date
  • Access automatically revokes after expiration

Scenario 3: Duplicate grant

Given:

  • Admin is authenticated with scope access-grants:write
  • User user_12345 already has READ access to case_abc123

When:

  • Admin attempts to grant READ access again:
    {
    "userId": "user_12345",
    "accessLevel": "READ"
    }

Then:

  • Response status is 409 Conflict
  • Response body contains:
    {
    "error": "DUPLICATE_GRANT",
    "message": "User 'user_12345' already has READ access to resource 'case:case_abc123'"
    }

Scenario 4: Upgrade existing grant

Given:

  • Admin is authenticated with scope access-grants:write
  • User has READ access to case

When:

  • Admin grants WRITE access to same user:
    {
    "userId": "user_12345",
    "accessLevel": "WRITE",
    "replaceExisting": true
    }

Then:

  • Response status is 201 Created
  • Previous READ grant is revoked
  • New WRITE grant is created
  • User now has WRITE access (not both)

Scenario 5: Resource not found

Given:

  • Admin is authenticated with scope access-grants:write
  • No case with ID case_nonexistent

When:

  • Admin POSTs to /admin/resources/case/case_nonexistent/access-grants

Then:

  • Response status is 404 Not Found
  • Response body contains:
    {
    "error": "NOT_FOUND",
    "message": "Resource 'case:case_nonexistent' not found"
    }

Scenario 6: User not found

Given:

  • Admin is authenticated with scope access-grants:write
  • Resource exists
  • No user with ID user_nonexistent

When:

  • Admin POSTs:
    {
    "userId": "user_nonexistent",
    "accessLevel": "READ"
    }

Then:

  • Response status is 404 Not Found
  • Response body contains:
    {
    "error": "NOT_FOUND",
    "message": "User with ID 'user_nonexistent' not found"
    }

Scenario 7: Invalid access level

Given:

  • Admin is authenticated with scope access-grants:write

When:

  • Admin POSTs with invalid access level:
    {
    "userId": "user_12345",
    "accessLevel": "INVALID"
    }

Then:

  • Response status is 400 Bad Request
  • Response body contains:
    {
    "error": "VALIDATION_ERROR",
    "message": "Invalid access level",
    "details": [
    {
    "field": "accessLevel",
    "message": "Must be one of: READ, WRITE, ADMIN"
    }
    ]
    }

Scenario 8: Expiration in past

Given:

  • Admin is authenticated with scope access-grants:write

When:

  • Admin POSTs with past expiration:
    {
    "userId": "user_12345",
    "accessLevel": "READ",
    "expiresAt": "2020-01-01T00:00:00Z"
    }

Then:

  • Response status is 400 Bad Request
  • Response body contains:
    {
    "error": "VALIDATION_ERROR",
    "message": "Expiration date must be in the future"
    }

Request Specification

Path Parameters

ParameterTypeRequiredDescription
typestringYesResource type (case, document, etc.)
idstringYesResource identifier

Request Body

FieldTypeRequiredConstraintsDescription
userIdstringYesValid user IDUser to grant access
accessLevelstringYesREAD, WRITE, ADMINLevel of access
expiresAtstringNoISO 8601, future dateOptional expiration timestamp
replaceExistingbooleanNoDefault: falseReplace existing grant if any

Validation Rules

RuleDescription
User uniquenessUser cannot have duplicate grant at same level (unless replaceExisting)
Future expirationexpiresAt must be in future if provided
Valid access levelMust be READ, WRITE, or ADMIN
Resource existsResource must exist in system
User existsUser must exist in system

Response Specification

Success Response (201 Created)

{
"id": "grant_xyz789",
"userId": "user_12345",
"resourceType": "case",
"resourceId": "case_abc123",
"accessLevel": "WRITE",
"grantedBy": "admin_789",
"grantedAt": "2025-10-19T10:00:00Z",
"expiresAt": "2026-01-19T10:00:00Z"
}

Response Fields

FieldTypeDescription
idstringGrant identifier (generated)
userIdstringUser who has access
resourceTypestringType of resource
resourceIdstringResource identifier
accessLevelstringLevel of access
grantedBystringAdmin who granted access
grantedAtstringGrant timestamp
expiresAtstring | nullExpiration timestamp

Error Responses

StatusError CodeDescription
400VALIDATION_ERRORInvalid input or expiration in past
401UNAUTHORIZEDMissing or invalid auth token
403FORBIDDENMissing access-grants:write scope
404NOT_FOUNDResource or user not found
409DUPLICATE_GRANTUser already has access at this level

Requirements Mapping

  • FR-174: Accept POST with userId and accessLevel
  • FR-175: Validate resource exists
  • FR-176: Validate user exists
  • FR-177: Validate access level is valid
  • FR-178: Support optional expiration timestamp
  • FR-179: Validate expiration is in future
  • FR-180: Prevent duplicate grants (same user, same level)
  • FR-181: Support replaceExisting to upgrade grants
  • FR-182: Record granting admin (grantedBy)
  • FR-183: Return complete grant details
  • FR-184: Return 409 for duplicate grants
  • FR-185: Require access-grants:write scope

Notes

Access Levels

  • READ: View/read resource only
  • WRITE: Modify/update resource
  • ADMIN: Full control including delete and manage permissions

Access levels are hierarchical in some systems (ADMIN > WRITE > READ).

Temporary Access

Use expiresAt for:

  • Guest access for external collaborators
  • Time-limited project access
  • Temporary elevated permissions
  • Compliance with least-privilege principle

Consider background job to cleanup expired grants.

Replacing Grants

With replaceExisting: true:

  • Useful for upgrading from READ to WRITE
  • Or downgrading from WRITE to READ
  • Atomically replaces previous grant
  • Maintains single grant per user per resource

Without it, attempting to upgrade results in 409 error.

Grant Inheritance

This endpoint creates grants on specific resources:

  • Does NOT grant access to parent resources
  • Does NOT grant access to subresources
  • Use separate endpoints for subresource grants
  • Or implement grant inheritance in authorization logic

Audit Trail

Each grant records:

  • Who granted it (grantedBy)
  • When it was granted (grantedAt)
  • When it expires (expiresAt)

Consider additional audit logging for compliance.