Skip to main content

WorkOwnershipService Deep Dive

Overview

The WorkOwnershipService provides centralized access control for works, enforcing role-based permissions across the platform. It determines whether a user can view, edit, manage members, or own a work by checking both creator status and membership records. Nearly every work operation in the system delegates authorization to this service.

Architecture

This service acts as a cross-cutting authorization layer used by all work-related services. It sits between API controllers and domain services, ensuring no operation proceeds without proper access verification.

Any Service / Controller
|
v
WorkOwnershipService.ensureAccess(workId, userId, minimumRole?)
|
+-- WorkRepository.findById()
|
+-- Check if user is creator (userId === work.userId)
| |
| YES --> Return OWNER role
| NO --> WorkMemberRepository.findMember()
| |
| +-- Member found --> Check role hierarchy
| +-- No member --> Throw ForbiddenException
|
v
WorkAccessResult { work, member, role, isCreator }

Role Hierarchy

The service enforces a four-level role hierarchy:

RoleLevelCapabilities
OWNER4Full control, can delete work
MANAGER3Can manage members and settings
EDITOR2Can modify work content
VIEWER1Read-only access

API Reference

Methods

ensureAccess(workId, userId, minimumRole?)

The core authorization method. Verifies a user has at least the specified minimum role.

ParameterTypeDescription
workIdstringThe work to check
userIdstringThe user requesting access
minimumRoleWorkMemberRole (optional)Minimum required role level

Returns: Promise<WorkAccessResult>

Throws:

  • NotFoundException if the work does not exist
  • ForbiddenException if the user lacks access or the required role level
interface WorkAccessResult {
work: Work;
member: WorkMember | null; // null for creators
role: WorkMemberRole;
isCreator: boolean;
}

ensureCanView(workId, userId)

Shorthand for ensureAccess(workId, userId, WorkMemberRole.VIEWER).

ensureCanEdit(workId, userId)

Shorthand for ensureAccess(workId, userId, WorkMemberRole.EDITOR).

ensureCanManageMembers(workId, userId)

Shorthand for ensureAccess(workId, userId, WorkMemberRole.MANAGER).

ensureIsOwner(workId, userId)

Shorthand for ensureAccess(workId, userId, WorkMemberRole.OWNER).

hasAccess(workId, userId)

Non-throwing access check. Returns true if the user has any level of access, false otherwise.

ParameterTypeDescription
workIdstringThe work to check
userIdstringThe user to check

Returns: Promise<boolean>

getUserRole(workId, userId)

Returns the user's role in a work without throwing exceptions.

ParameterTypeDescription
workIdstringThe work to check
userIdstringThe user to check

Returns: Promise<WorkMemberRole | null> -- null if no access.

Implementation Details

Creator Privilege

The work creator (identified by work.userId) always receives OWNER-level access regardless of membership records. This is checked before any membership lookups, making it the fast path for the most common case.

Role Comparison

The roleIsAtLeast() helper uses a numeric hierarchy map to compare roles:

const roleHierarchy: Record<WorkMemberRole, number> = {
OWNER: 4,
MANAGER: 3,
EDITOR: 2,
VIEWER: 1
};

A role satisfies a minimum requirement when its numeric value is greater than or equal to the minimum role's value.

Non-Throwing Pattern

The hasAccess() method wraps ensureAccess() in a try-catch, returning a boolean. This pattern is useful for conditional UI rendering or optional permission checks without disrupting control flow.

Database Interactions

RepositoryMethodPurpose
WorkRepositoryfindById(workId)Load the work entity to check creator status
WorkMemberRepositoryfindMember(workId, userId)Look up membership record for non-creator users

Event System

This service does not emit or consume any events. It is a pure authorization service.

Error Handling

ScenarioExceptionHTTP Status
Work not foundNotFoundException404
User not creator and not a memberForbiddenException403
User role below minimum requirementForbiddenException403

All exceptions include structured error bodies with status: 'error' and a descriptive message.

Usage Examples

// Check editor access before modifying taxonomy
const { work } = await this.ownershipService.ensureCanEdit(workId, userId);

// Non-throwing check for conditional logic
const canAccess = await this.ownershipService.hasAccess(workId, userId);
if (canAccess) {
// show work in list
}

// Get role for UI rendering
const role = await this.ownershipService.getUserRole(workId, userId);
if (role === WorkMemberRole.OWNER) {
// show delete button
}

// Full access result with member info
const result = await this.ownershipService.ensureAccess(workId, userId);
console.log(result.isCreator); // true if they created it
console.log(result.role); // 'owner', 'manager', 'editor', or 'viewer'

Configuration

This service has no configuration. Role hierarchy is hardcoded as it represents a fundamental domain invariant.