Permissions Architecture
Source of Truth: Role.permissions JSON
Permissions in this application flow from WoW guild ranks to CASL abilities:
flowchart LR
A[WoW API] --> B[character.service.ts]
B --> C["Role.permissions (JSON)"]
C --> D[ability.factory.ts]
D --> E[CASL rules]
How It Works
1. WoW API Sync Sets Permissions
When characters are synced from Battle.net (character.service.ts), guild roles are created/updated with permissions based on WoW rank:
// character.service.ts - createMembershipWithRankName()
if (wowRank === 0) {
permissions = {
canManageGuild: true,
canManageMembers: true,
canManageEvents: true,
};
} else if (wowRank === 1) {
permissions = {
canManageGuild: true,
canManageMembers: true,
canManageEvents: true,
};
} else if (wowRank === 2) {
permissions = { canManageMembers: true, canManageEvents: true };
} else {
permissions = {};
}
These permissions are stored in Role.permissions (JSONB field in database).
2. CASL Reads from Role.permissions
The AbilityFactory reads Role.permissions and converts them to CASL abilities:
// ability.factory.ts - createForGuildMember()
const permissions = membership.role.permissions || {};
if (permissions.canManageGuild) {
can([Action.Update], 'Guild', { id: membership.guildId });
can([Action.Create, Action.Update, Action.Delete], 'Role', {
guildId: membership.guildId,
});
}
if (permissions.canManageMembers) {
can([Action.Update, Action.Invite, Action.Kick], 'Membership', {
guildId: membership.guildId,
});
}
if (permissions.canManageEvents) {
can([Action.Create, Action.Update, Action.Delete], 'Event', {
guildId: membership.guildId,
});
can(Action.Read, 'AttendanceStats');
}
if (permissions.canViewAttendance) {
can(Action.Read, 'AttendanceStats');
}
3. Frontend Uses CASL Abilities
The frontend receives serialized ability rules and uses them to show/hide UI:
<Can I={Action.Update} a="Guild">
<GuildSettings />
</Can>
Permission Levels by WoW Rank
Rank 0: Guild Master
canManageGuild: true→ Can update guild settings, manage rolescanManageMembers: true→ Can invite/kick memberscanManageEvents: true→ Can create/update/delete events (impliescanViewAttendance)canViewAttendance: true→ Can view attendance statistics- Special: Gets
Action.Manageon'all'(full control)
Rank 1: Top Officer
canManageGuild: true→ Can update guild settings, manage rolescanManageMembers: true→ Can invite/kick memberscanManageEvents: true→ Can create/update/delete events (impliescanViewAttendance)canViewAttendance: true→ Can view attendance statistics
Rank 2: Officer
canManageGuild: false→ Cannot update guild settingscanManageMembers: true→ Can invite/kick memberscanManageEvents: true→ Can create/update/delete events (impliescanViewAttendance)canViewAttendance: true→ Can view attendance statistics
Rank 3+: Regular Members
canManageGuild: falsecanManageMembers: falsecanManageEvents: falsecanViewAttendance: false(can be granted independently)- Read-only access (subject to guild privacy settings)
Sync Source Restrictions
CASL permissions determine what a user is allowed to do, but the guild's syncSource determines which management features exist at all. For Blizzard-synced guilds:
- Member role changes and removal are blocked (managed in-game)
- Custom role creation is blocked (roles come from WoW ranks)
- The UI hides these controls; the backend rejects attempts via API
These restrictions are enforced separately from CASL — a user may have canManageMembers: true but still cannot change roles in a Blizzard guild because the operation itself is not permitted for that guild type.
Why This Architecture?
Benefits
- Single Source of Truth:
Role.permissionsis authoritative - WoW-Aligned: Permissions match WoW guild rank structure
- Extensible: Can add custom permissions in the future
- Auditable: Permissions are stored in DB, changes are tracked
- Flexible: Guild leaders could theoretically customize permissions
Separation of Concerns
character.service.ts: Syncs from WoW, sets permissionsability.factory.ts: Reads permissions, creates CASL abilitieswowRank: Used for display/sorting, not for authorization logic
Guild Privacy Settings
Guild-specific settings (like rosterPrivacy) are applied on top of role permissions:
const rosterPrivacy = guildSettings.rosterPrivacy || 'members';
if (rosterPrivacy === 'officers' && wowRank > 2) {
// Block regular members from seeing roster
cannot(Action.Read, 'Membership');
}
This allows guilds to be more restrictive than the default role permissions.
Future: Custom Permissions
In the future, guild leaders could customize role permissions beyond WoW sync:
// Hypothetical: Guild leader overrides permissions for a specific role
await prisma.role.update({
where: { id: roleId },
data: {
permissions: {
canManageGuild: false,
canManageMembers: true,
canManageEvents: true,
canManageLoot: true, // Custom permission
},
},
});
CASL would automatically respect these custom permissions.
Implementation Checklist
When adding a new permission:
- ✅ Add to
Role.permissionsJSON structure (character.service.ts) - ✅ Add corresponding CASL rule in
ability.factory.ts - ✅ Use
@Cancomponent orability.can()in frontend - ✅ Add guard decorator to backend endpoints
Example:
// 1. character.service.ts
if (wowRank === 0) {
permissions = { ..., canManageLoot: true };
}
// 2. ability.factory.ts
if (permissions.canManageLoot) {
can([Action.Create, Action.Update], 'LootDistribution');
}
// 3. Frontend
<Can I={Action.Update} a="LootDistribution">
<LootManager />
</Can>
// 4. Backend
@Put('loot/:id')
@RequireAbilities({ action: Action.Update, subject: 'LootDistribution' })
updateLoot() { }
Troubleshooting
Problem: User has rank 1 but can't access guild settings
Check:
-
Does their
Role.permissionsincludecanManageGuild: true?SELECT r.name, r."wowRank", r.permissions
FROM roles r
WHERE r.id = 'role-id'; -
Are they the highest-ranked character for that user in the guild?
-
Was the character synced recently? (Old syncs might have outdated permissions)
Solution: Re-sync the character to update role permissions from WoW.