The different user types:
export const enum USER_TYPE {
SUPER_ADMIN = 'SUPER_ADMIN',
ADMIN = 'ADMIN',
COMPANY_ADMIN = 'COMPANY_ADMIN',
STORE_ADMIN = 'STORE_ADMIN',
MANAGER = 'MANAGER',
}
The AppAbility code using CASL approach
type Actions = 'manage' | 'create' | 'read' | 'update' | 'delete';
type Subjects = InferSubjects<typeof User> | 'all';
type AppAbility = MongoAbility<[Actions, Subjects]>;
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
@Injectable()
export class UserAbilityFactory {
createForUser(userType: USER_TYPE): MongoAbility {
try {
if (userType === USER_TYPE.SUPER_ADMIN) {
can('manage', 'all');
}
if (userType === USER_TYPE.ADMIN) {
cannot('create', User, {
userType: {
$eq: USER_TYPE.SUPER_ADMIN,
},
});
can('create', User, {
userType: {
$in: [USER_TYPE.COMPANY_ADMIN, USER_TYPE.STORE_ADMIN],
},
});
}
return build();
} catch (error) {
throw error;
}
}
}
I want the ADMIN user to be able to create users other than SUPER_ADMIN but it creates a SUPER_ADMIN user
The user registration code:
@Post('register')
@UseGuards(RolesGuard)
@Roles(USER_TYPE.SUPER_ADMIN, USER_TYPE.ADMIN, USER_TYPE.COMPANY_ADMIN)
@HttpCode(201)
async register(@Body() userData: CreateUserDto): Promise<any> {
try {
const user = await firstValueFrom(this.userService.Signup(userData));
return sendSuccess(user);
} catch (error) {
throw new BadRequestException(error.message);
}
}
The AuthGuard code:
export const Roles = (...roles: USER_TYPE[]) => SetMetadata('roles', roles);
@Injectable()
export class RolesGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly jwtService: JwtService,
private readonly redisService: RedisService,
private readonly userAbilityFactory: UserAbilityFactory
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.get<USER_TYPE[]>('roles', context.getHandler());
if (!requiredRoles) return true;
const request = context.switchToHttp().getRequest();
const bearerToken = request.headers.authorization;
if (!bearerToken || !bearerToken.startsWith('Bearer ')) return false;
const token = bearerToken.split(' ')[1];
try {
const decoded = this.jwtService.verifyAccessToken(token);
const userRole = decoded.userType;
const ability = this.userAbilityFactory.createForUser(userRole);
const canAccess = ability.can('create', User);
if (canAccess) {
request.user = decoded;
const userId = decoded.userId;
const loggedIn = await this.redisService.getValue(userId);
if (!loggedIn) return false;
return true;
}
return false;
} catch (error) {
return false;
}
}
}
The canAccess variable always returns true.
What is it that I am doing wrong? I don't understand that. Does my code even check that what kind of user I want to create from the request body?
I'm writing an answer here, so that I have more space. It might not be complete, but hopefully point you in the right direction.
I quickly put together a working example based on your code here https://codesandbox.io/p/sandbox/cocky-nash-ytfpyj.
Important to note here is
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility);needs to be called inside your functioncreateForUser. Otherwise the abilities will accumulate over several calls of that method, because the builder is created outside the function scope. You can test this in the sandbox if you move the creation of the builder outside the function, which will give you unexpected results."User"instead ofUser. With the latter the conditions just don't work. For using strings you need to define the Subjects astype Subjects = User | "User" | "all";ability.can(), because you want to know if your current user can create a specific user payload.request.bodywhich is of typeCreateUserDtolike thisability.can('create', request.body), you might need to change everything related to theUserclass in the sandbox to theCreateUserDto.userType-> make sure yourUserorUserCreateDtoclass also has the fielduserType. In your other question, theUserclass had a field namedtypeonly. That way the matching of course cannot work.Hope this gets you closer to your solution :)