Case Study 1: Building a Permission System with Sets

The Scenario

You are developing an internal file management system for a small company. Different users have different levels of access: interns can only read files, regular employees can read and write, team leads can also execute scripts and delete files, and administrators have full access. The system must check permissions before allowing any operation and support combining permissions from multiple roles.

This is a classic problem that appears in operating systems, web applications, and database systems. Pascal's sets provide an elegant, efficient, and type-safe solution.

Defining the Permission Types

We start with an enumerated type for individual permissions and a set type for permission collections:

program PermissionSystem;

{$mode objfpc}{$H+}
{$R+}

type
  TPermission = (
    permRead,
    permWrite,
    permExecute,
    permDelete,
    permAdmin
  );
  TPermissions = set of TPermission;

  TRole = (
    roleIntern,
    roleEmployee,
    roleTeamLead,
    roleAdmin
  );

We prefix permission values with perm and role values with role to avoid any naming ambiguity. The TPermissions type is a set that can hold any combination of the five permissions — from no permissions at all ([]) to full access ([permRead..permAdmin]).

Mapping Roles to Permissions

Each role has a default set of permissions. We can express this cleanly with a function:

function RolePermissions(R: TRole): TPermissions;
begin
  case R of
    roleIntern:   Result := [permRead];
    roleEmployee: Result := [permRead, permWrite];
    roleTeamLead: Result := [permRead, permWrite, permExecute, permDelete];
    roleAdmin:    Result := [permRead, permWrite, permExecute, permDelete, permAdmin];
  end;
end;

Notice how each higher role includes all the permissions of the lower roles plus additional ones. This is a natural hierarchy, and the set representation makes it explicit.

The User Record

A user has a name, a primary role, and an effective permission set. The effective permissions may differ from the role defaults if permissions have been individually granted or revoked:

type
  TUser = record
    Name: string;
    Role: TRole;
    EffectivePerms: TPermissions;
  end;

function CreateUser(const AName: string; ARole: TRole): TUser;
begin
  Result.Name := AName;
  Result.Role := ARole;
  Result.EffectivePerms := RolePermissions(ARole);
end;

Permission Checking

The core of the system is checking whether a user has the required permissions for an operation:

function HasPermission(const User: TUser; Required: TPermission): Boolean;
begin
  Result := Required in User.EffectivePerms;
end;

function HasAllPermissions(const User: TUser; Required: TPermissions): Boolean;
begin
  Result := Required <= User.EffectivePerms;
end;

The HasAllPermissions function uses the subset operator (<=). If every permission in Required is also in User.EffectivePerms, the user is authorized. This single line replaces what would be a loop or chain of and conditions in other approaches.

Granting and Revoking Permissions

Sometimes an individual user needs permissions beyond their role, or specific permissions need to be revoked:

procedure GrantPermission(var User: TUser; P: TPermission);
begin
  Include(User.EffectivePerms, P);
  WriteLn('Granted ', PermissionToStr(P), ' to ', User.Name);
end;

procedure RevokePermission(var User: TUser; P: TPermission);
begin
  Exclude(User.EffectivePerms, P);
  WriteLn('Revoked ', PermissionToStr(P), ' from ', User.Name);
end;

procedure ResetToRoleDefaults(var User: TUser);
begin
  User.EffectivePerms := RolePermissions(User.Role);
  WriteLn('Reset ', User.Name, ' to default ', RoleToStr(User.Role), ' permissions');
end;

The Include and Exclude procedures map to single bit operations — setting or clearing one bit in the permission bit vector. There is no allocation, no searching, no hash computation.

Display Functions

We need helper functions to convert our enumerated values to readable strings:

function PermissionToStr(P: TPermission): string;
begin
  case P of
    permRead:    Result := 'Read';
    permWrite:   Result := 'Write';
    permExecute: Result := 'Execute';
    permDelete:  Result := 'Delete';
    permAdmin:   Result := 'Admin';
  end;
end;

function RoleToStr(R: TRole): string;
begin
  case R of
    roleIntern:   Result := 'Intern';
    roleEmployee: Result := 'Employee';
    roleTeamLead: Result := 'Team Lead';
    roleAdmin:    Result := 'Administrator';
  end;
end;

procedure DisplayPermissions(const User: TUser);
var
  P: TPermission;
  First: Boolean;
begin
  Write(User.Name, ' (', RoleToStr(User.Role), '): [');
  First := True;
  for P := Low(TPermission) to High(TPermission) do
    if P in User.EffectivePerms then
    begin
      if not First then Write(', ');
      Write(PermissionToStr(P));
      First := False;
    end;
  WriteLn(']');
end;

Simulating File Operations

Now we can build the operation-checking layer:

procedure AttemptOperation(const User: TUser; Op: TPermission;
                           const FileName: string);
begin
  Write(User.Name, ' attempting to ', PermissionToStr(Op), ' "', FileName, '": ');
  if HasPermission(User, Op) then
    WriteLn('ALLOWED')
  else
    WriteLn('DENIED — requires ', PermissionToStr(Op), ' permission');
end;

procedure AttemptAdminTask(const User: TUser; const TaskName: string);
var
  Required: TPermissions;
begin
  Required := [permAdmin, permWrite];
  Write(User.Name, ' attempting admin task "', TaskName, '": ');
  if HasAllPermissions(User, Required) then
    WriteLn('ALLOWED')
  else
    WriteLn('DENIED — requires Admin + Write permissions');
end;

Combining Permissions from Multiple Roles

In real systems, a user may hold multiple roles. We can combine permissions using set union:

function CombineRolePermissions(Role1, Role2: TRole): TPermissions;
begin
  Result := RolePermissions(Role1) + RolePermissions(Role2);
end;

For example, a user who is both an Employee and a part-time security auditor (with Execute permissions) can have:

var
  Auditor: TUser;
begin
  Auditor := CreateUser('Chen Wei', roleEmployee);
  Auditor.EffectivePerms := Auditor.EffectivePerms + [permExecute];
  { Now has Read, Write, Execute }
end.

Putting It All Together

Here is the main program that demonstrates the full system:

var
  Intern, Employee, Lead, Admin: TUser;
begin
  WriteLn('=== Permission System Demo ===');
  WriteLn;

  { Create users }
  Intern   := CreateUser('Alex (Intern)', roleIntern);
  Employee := CreateUser('Jordan (Employee)', roleEmployee);
  Lead     := CreateUser('Sam (Team Lead)', roleTeamLead);
  Admin    := CreateUser('Pat (Admin)', roleAdmin);

  { Display default permissions }
  WriteLn('--- Default Permissions ---');
  DisplayPermissions(Intern);
  DisplayPermissions(Employee);
  DisplayPermissions(Lead);
  DisplayPermissions(Admin);
  WriteLn;

  { Test operations }
  WriteLn('--- Operation Checks ---');
  AttemptOperation(Intern, permRead, 'report.txt');
  AttemptOperation(Intern, permWrite, 'report.txt');
  AttemptOperation(Employee, permWrite, 'report.txt');
  AttemptOperation(Employee, permDelete, 'old-data.csv');
  AttemptOperation(Lead, permDelete, 'old-data.csv');
  AttemptOperation(Lead, permAdmin, 'system.conf');
  WriteLn;

  { Grant special permission }
  WriteLn('--- Permission Modification ---');
  GrantPermission(Intern, permWrite);
  DisplayPermissions(Intern);
  AttemptOperation(Intern, permWrite, 'notes.txt');
  WriteLn;

  { Revoke permission }
  RevokePermission(Intern, permWrite);
  DisplayPermissions(Intern);
  AttemptOperation(Intern, permWrite, 'notes.txt');
  WriteLn;

  { Admin task requiring multiple permissions }
  WriteLn('--- Admin Tasks ---');
  AttemptAdminTask(Employee, 'Reset user password');
  AttemptAdminTask(Admin, 'Reset user password');

  WriteLn;
  WriteLn('=== Demo Complete ===');
end.

Expected Output

=== Permission System Demo ===

--- Default Permissions ---
Alex (Intern) (Intern): [Read]
Jordan (Employee) (Employee): [Read, Write]
Sam (Team Lead) (Team Lead): [Read, Write, Execute, Delete]
Pat (Admin) (Administrator): [Read, Write, Execute, Delete, Admin]

--- Operation Checks ---
Alex (Intern) attempting to Read "report.txt": ALLOWED
Alex (Intern) attempting to Write "report.txt": DENIED — requires Write permission
Jordan (Employee) attempting to Write "report.txt": ALLOWED
Jordan (Employee) attempting to Delete "old-data.csv": DENIED — requires Delete permission
Sam (Team Lead) attempting to Delete "old-data.csv": ALLOWED
Sam (Team Lead) attempting to Admin "system.conf": DENIED — requires Admin permission

--- Permission Modification ---
Granted Write to Alex (Intern)
Alex (Intern) (Intern): [Read, Write]
Alex (Intern) attempting to Write "notes.txt": ALLOWED

Revoked Write from Alex (Intern)
Alex (Intern) (Intern): [Read]
Alex (Intern) attempting to Write "notes.txt": DENIED — requires Write permission

--- Admin Tasks ---
Jordan (Employee) attempting admin task "Reset user password": DENIED — requires Admin + Write permissions
Pat (Admin) attempting admin task "Reset user password": ALLOWED

=== Demo Complete ===

Analysis

Why Sets Work Here

The permission system demonstrates several strengths of Pascal sets:

  1. Type safety. You cannot accidentally assign a TRole value to a TPermission variable. The compiler catches these errors at compile time.

  2. Readability. Required <= User.EffectivePerms reads almost like English: "the required permissions are a subset of the user's effective permissions."

  3. Efficiency. Every permission check is a single bit test. Combining permissions is a single OR instruction. The entire permission set for any user fits in a single byte.

  4. Maintainability. Adding a new permission (say, permCreate) requires adding one value to the enumeration and updating the role-mapping function. Every case statement that does not handle the new value will generate a compiler warning.

Comparison with Other Approaches

Without sets, you would need either an array of booleans (Permissions: array[0..4] of Boolean) or bitwise integer operations (Permissions: Integer with PERM_READ = 1, PERM_WRITE = 2, etc.). The array approach wastes memory and requires loops for comparisons. The bitwise approach requires the programmer to manually assign powers of two and remember which bit is which. Pascal sets give you the efficiency of bit operations with the clarity of named values.

Exercises for This Case Study

  1. Add a permCreate permission that allows creating new files. Update the role permissions so that Employees and above can create files.

  2. Implement an audit log: every time a permission is checked, record the user, the operation, the file, and whether it was allowed or denied. Store the log as an array of records.

  3. Add a DemoteUser procedure that moves a user to a lower role and adjusts their effective permissions, keeping any individually granted extra permissions that are valid for the new role.

  4. Implement "temporary elevation": a procedure that grants Admin permissions for a specific number of operations, then automatically revokes them. (Hint: use a counter alongside the permission set.)