Reputation: 73
I'm working on implementing user permissions in my NestJs TypeScript project using CASL. I have two types of users: editors and members. editors should be able to edit any column of any user, while members should only be allowed to edit their own profile information, specifically the "name" field. How can I achieve this using CASL, and how can I efficiently check the user's role within controllers or services to enforce these permissions?
Here's a brief overview of my project structure:
I have a casl.ts file where I define abilities and create permissions. Database entities are defined in files such as permission.entity.ts, role.entity.ts, and user.entity.ts. My goal is to consolidate the update functionality for both editor and member roles into a single function, such as "updateUser," instead of having separate functions like "updateUserByMember" and "updateUserByEditor." How can I achieve this, and how can CASL be utilized to ensure that members can only update the "name" field of their own profiles?
I appreciate any insights or examples on how to implement this efficiently within CASL and how to handle role-based access control within controllers or services. Thank you!
File: casl.ts
import { User } from './users/entities/user.entity';
import {
ForcedSubject,
MongoAbility,
RawRuleOf,
createMongoAbility,
} from '@casl/ability';
import { get } from 'lodash';
import { Permission } from './database/entities/permission.entity';
export const actions = [
'manage',
'create',
'read',
'update',
'delete',
] as const;
export const subjects = ['User', 'Post', 'all'] as const;
export type AppAbilities = [
(typeof actions)[number],
(
| (typeof subjects)[number]
| ForcedSubject<Exclude<(typeof subjects)[number], 'all'>>
),
];
export type AppAbility = MongoAbility<AppAbilities>;
export const createAbility = (rules: RawRuleOf<AppAbility>[]) => {
return createMongoAbility<AppAbilities>(rules);
};
export const createUserAbility = (user: User, vars: any = {}) => {
if (!vars.hasOwnProperty('user')) {
vars.user = user;
}
return createAbility(createUserPermissions(user, vars));
};
function interpolate(template: string, vars: object) {
return JSON.parse(template, (_, rawValue) => {
if (rawValue[0] !== '$') {
return rawValue;
}
const name = rawValue.slice(2, -1);
const value = get(vars, name);
if (typeof value === 'undefined') {
throw new ReferenceError(`Variable ${name} is not defined`);
}
return value;
});
}
function createUserPermissions(user: User, vars: object = {}) {
const permissions = user.roles.reduce((permissions, role) => {
return [...permissions, ...normalizePermissions(role.permissions)];
}, []);
return interpolate(JSON.stringify(permissions), vars);
}
function normalizePermissions(permissions: Permission[]) {
return permissions.map((permission) => {
const { fields, ...rest } = permission;
return fields && fields.length ? { ...rest, fields } : rest;
});
}
Database:
File: permission.entity.ts:
import {
Column,
Entity,
ManyToMany,
PrimaryGeneratedColumn,
Unique,
} from 'typeorm';
import { Role } from './role.entity';
@Entity('permissions')
@Unique(['action', 'subject'])
export class Permission {
@PrimaryGeneratedColumn()
id: number;
@Column()
action: string;
@Column()
subject: string;
@Column('varchar', { array: true, default: [] })
fields: string[];
@Column({ type: 'jsonb', nullable: true })
conditions: object;
@ManyToMany(() => Role, (role) => role.permissions)
roles: Role[];
}
File: role.entity.ts:
import {
Column,
Entity,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Permission } from './permission.entity';
@Entity('roles')
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
name: string;
@ManyToMany(() => User, (user) => user.roles)
users: User[];
@ManyToMany(() => Permission, (permission) => permission.roles)
@JoinTable()
permissions: Permission[];
}
File: user.entity.ts:
import {
Column,
Entity,
JoinTable,
ManyToMany,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Role } from '../../database/entities/role.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column()
name: string;
@ManyToMany(() => Role, (role) => role.users)
@JoinTable()
roles: Role[];
}
Upvotes: 0
Views: 343