Can't use CASL, using custom perms
This commit is contained in:
parent
c66e93df96
commit
8e3383fd1c
|
@ -1,15 +1,6 @@
|
|||
import { Controller, Get, Redirect } from '@nestjs/common';
|
||||
import { ApiMovedPermanentlyResponse, ApiProperty } from '@nestjs/swagger';
|
||||
import { AppService } from './app.service';
|
||||
import { Guest } from './auth/guest.decorator';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Guest()
|
||||
@Get()
|
||||
@ApiMovedPermanentlyResponse({ description: "Redirect to /api" })
|
||||
@Redirect("/api")
|
||||
home(): void { }
|
||||
constructor() {}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import { CaslModule } from './casl/casl.module';
|
|||
password: configService.get("DB_PASS", ""),
|
||||
database: configService.get("DB_NAME", "test"),
|
||||
autoLoadModels: true,
|
||||
logging: configService.get("APP_ENV", "development") === "development" ? console.log : false,
|
||||
synchronize: configService.get("APP_ENV", "development") === "development" ? true : false
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
|
@ -36,7 +38,7 @@ import { CaslModule } from './casl/casl.module';
|
|||
CaslModule
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, {
|
||||
providers: [{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
}],
|
||||
|
|
|
@ -24,7 +24,7 @@ import { LocalStrategy } from './local.strategy';
|
|||
inject: [ConfigService]
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, UsersService, ConfigService, LocalStrategy, JwtStrategy],
|
||||
providers: [AuthService, UsersService, ConfigService, LocalStrategy, JwtStrategy, UsersService],
|
||||
exports: [AuthService]
|
||||
})
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@ import { ExtractJwt, Strategy } from 'passport-jwt';
|
|||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UsersService } from '../users/users.service';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(protected configService: ConfigService) {
|
||||
constructor(protected configService: ConfigService, private usersService: UsersService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
|
@ -14,6 +15,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||
}
|
||||
|
||||
async validate(payload: any) {
|
||||
return { userId: payload.sub, username: payload.username };
|
||||
const user = await this.usersService.findOne(payload.sub);
|
||||
return { userId: payload.sub, username: payload.username, data: user };
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { subject } from "@casl/ability";
|
||||
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { User } from "src/users/models/user.model";
|
||||
|
@ -20,7 +21,7 @@ export class PoliciesGuard implements CanActivate {
|
|||
) || [];
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
let userDb = await User.findByPk(user.userId);
|
||||
const userDb = await User.findByPk(user.userId);
|
||||
const ability = this.caslAbilityFactory.createForUser(userDb);
|
||||
|
||||
return policyHandlers.every((handler) =>
|
||||
|
|
|
@ -1,7 +1,71 @@
|
|||
import { CaslAbilityFactory } from './casl-ability.factory';
|
||||
import { Sequelize } from "sequelize-typescript";
|
||||
import { Action } from "../auth/enums";
|
||||
import { Company } from "../companies/models/company.model";
|
||||
import { CompanyEarn } from "../companies/models/companyEarn.model";
|
||||
import { User } from "../users/models/user.model";
|
||||
import { CaslAbilityFactory } from "./casl-ability.factory";
|
||||
|
||||
|
||||
describe('CaslAbilityFactory', () => {
|
||||
it('should be defined', () => {
|
||||
expect(new CaslAbilityFactory()).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Permissions", () => {
|
||||
new Sequelize({
|
||||
validateOnly: true,
|
||||
models: [User, Company, CompanyEarn] // don't forget to add your models like this... or [User, ...]
|
||||
});
|
||||
|
||||
let user: User;
|
||||
let user2: User;
|
||||
let ability;
|
||||
const caslAbilityFactoryInstance = new CaslAbilityFactory();
|
||||
|
||||
describe("when user is an admin", () => {
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
user.firstName = "John";
|
||||
user.lastName = "Doe";
|
||||
user.email = "email@domain.tld";
|
||||
user.isAdmin = true;
|
||||
|
||||
ability = caslAbilityFactoryInstance.createForUser(user);
|
||||
});
|
||||
|
||||
it("can do anything", () => {
|
||||
expect(ability.can(Action.Manage, "all")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when user is a not admin", () => {
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
user.firstName = "John";
|
||||
user.lastName = "Doe";
|
||||
user.email = "email@domain.tld";
|
||||
user.isAdmin = false;
|
||||
|
||||
user2 = new User();
|
||||
user2.firstName = "Maria";
|
||||
user2.lastName = "Doe";
|
||||
user2.email = "email@domain.tld";
|
||||
user2.isAdmin = false;
|
||||
|
||||
ability = caslAbilityFactoryInstance.createForUser(user);
|
||||
});
|
||||
|
||||
it("should return user model name", () => {
|
||||
expect(user.modelName).toBe("User");
|
||||
})
|
||||
|
||||
it("can read self data", () => {
|
||||
expect(ability.can(Action.Read, user)).toBe(true);
|
||||
});
|
||||
|
||||
it("cannot read other user data", () => {
|
||||
expect(ability.can(Action.Read, user2)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,11 +1,10 @@
|
|||
import { Ability, AbilityBuilder, AbilityClass, ExtractSubjectType, InferSubjects } from "@casl/ability";
|
||||
import { Ability, AbilityBuilder, AbilityClass } from "@casl/ability";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Action } from "src/auth/enums";
|
||||
import { Company } from "src/companies/models/company.model";
|
||||
import { User } from "src/users/models/user.model";
|
||||
import { Action } from "../auth/enums";
|
||||
import { User } from "../users/models/user.model";
|
||||
|
||||
type Subjects = InferSubjects<typeof Company | typeof User> | 'all';
|
||||
|
||||
type Subjects = 'Company' | 'User' | 'all';
|
||||
export type AppAbility = Ability<[Action, Subjects]>;
|
||||
|
||||
@Injectable()
|
||||
|
@ -17,24 +16,21 @@ export class CaslAbilityFactory {
|
|||
|
||||
if (user.isAdmin) {
|
||||
can(Action.Manage, 'all'); // read-write access to everything
|
||||
}else{
|
||||
// Everything rules
|
||||
can(Action.Create, 'all');
|
||||
|
||||
// User rules
|
||||
can(Action.Read, 'User', { id: user.id }).because("User can only read its own data");
|
||||
can(Action.Update, 'User', { id: user.id });
|
||||
can(Action.Delete, 'User', { id: user.id });
|
||||
|
||||
// Company rules
|
||||
can(Action.Read, 'Company', { user: user.id });
|
||||
can(Action.Update, 'Company', { user: user.id });
|
||||
can(Action.Delete, 'Company', { user: user.id });
|
||||
}
|
||||
|
||||
// Everything rules
|
||||
can(Action.Create, 'all');
|
||||
|
||||
// User rules
|
||||
can(Action.Read, User, { id: user.id });
|
||||
can(Action.Update, User, { id: user.id });
|
||||
can(Action.Delete, User, { id: user.id });
|
||||
|
||||
// Company rules
|
||||
can(Action.Read, Company, { user: user.id });
|
||||
can(Action.Update, Company, { user: user.id });
|
||||
can(Action.Delete, Company, { user: user.id });
|
||||
|
||||
return build({
|
||||
// Read https://casl.js.org/v5/en/guide/subject-type-detection#use-classes-as-subject-types for details
|
||||
detectSubjectType: item => item.constructor as ExtractSubjectType<Subjects>
|
||||
});
|
||||
return build();
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ export class CompaniesController {
|
|||
@ApiInternalServerErrorResponse({ description: "Internal server error" })
|
||||
@Get()
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Company))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'Company'))
|
||||
findAll() {
|
||||
return this.companiesService.findAll();
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ export class CompaniesController {
|
|||
@ApiInternalServerErrorResponse({ description: "Internal server error" })
|
||||
@Get(':id')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Company))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'Company'))
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.companiesService.findOne(+id);
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ export class CompaniesController {
|
|||
@ApiInternalServerErrorResponse({ description: "Internal server error" })
|
||||
@Patch(':id')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Update, Company))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Update, 'Company'))
|
||||
update(@Param('id') id: string, @Body() updateCompanyDto: UpdateCompanyDto) {
|
||||
return this.companiesService.update(+id, updateCompanyDto);
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export class CompaniesController {
|
|||
@ApiInternalServerErrorResponse({ description: "Internal server error" })
|
||||
@Delete(':id')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Delete, Company))
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Delete, 'Company'))
|
||||
remove(@Param('id') id: string) {
|
||||
return this.companiesService.remove(+id);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AllowNull, BelongsTo, Column, CreatedAt, HasMany, Length, Model, NotEmpty, Table, UpdatedAt } from 'sequelize-typescript';
|
||||
import { User } from 'src/users/models/user.model';
|
||||
import { CompanyEarn } from './companyEarn.model';
|
||||
import { Table, Model, AllowNull, NotEmpty, Length, Column, BelongsTo, HasMany, CreatedAt, UpdatedAt } from "sequelize-typescript";
|
||||
import { User } from "../../users/models/user.model";
|
||||
import { CompanyEarn } from "./companyEarn.model";
|
||||
|
||||
@Table
|
||||
export class Company extends Model {
|
||||
|
@ -25,4 +25,8 @@ export class Company extends Model {
|
|||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
static get modelName() {
|
||||
return "Company";
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import sequelize from 'sequelize';
|
||||
import { AllowNull, Column, CreatedAt, DefaultScope, HasMany, IsDate, IsEmail, Length, Model, NotEmpty, Scopes, Table, Unique, UpdatedAt } from 'sequelize-typescript';
|
||||
import { Company } from '../../companies/models/company.model';
|
||||
import sequelize from "sequelize";
|
||||
import { DefaultScope, Scopes, Table, Model, AllowNull, NotEmpty, Length, Column, IsEmail, Unique, IsDate, HasMany, CreatedAt, UpdatedAt } from "sequelize-typescript";
|
||||
import { Company } from "../../companies/models/company.model";
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
@DefaultScope(() => ({
|
||||
|
@ -77,4 +77,8 @@ export class User extends Model {
|
|||
|
||||
@UpdatedAt
|
||||
updatedAt: Date;
|
||||
|
||||
static get modelName() {
|
||||
return "User";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const getUser = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user.data;
|
||||
},
|
||||
);
|
|
@ -1,4 +1,4 @@
|
|||
import { Controller, Get, Post, Body, Patch, Param, Delete, Request, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Request, UseGuards, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
|
@ -14,6 +14,7 @@ import { PoliciesGuard } from 'src/casl/PoliciesGuard';
|
|||
import { CheckPolicies } from 'src/casl/check-policy.decorator';
|
||||
import { AppAbility } from 'src/casl/casl-ability.factory';
|
||||
import { Action } from 'src/auth/enums';
|
||||
import { getUser } from './user.decorator';
|
||||
|
||||
@ApiTags("Users")
|
||||
@ApiBearerAuth()
|
||||
|
@ -56,10 +57,13 @@ export class UsersController {
|
|||
@ApiOkResponse({ description: "Success, return users data array", type: [PublicUserDto] })
|
||||
@ApiUnauthorizedResponse({ description: "Unauthorized" })
|
||||
@ApiInternalServerErrorResponse({ description: "Internal server error" })
|
||||
@Get()
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, User))
|
||||
findAll() {
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'User'))
|
||||
@Get()
|
||||
findAll(@getUser() auth: User) {
|
||||
if(!auth.isAdmin)
|
||||
return new ForbiddenException("Access denied.");
|
||||
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
|
||||
|
@ -69,9 +73,18 @@ export class UsersController {
|
|||
@ApiOperation({summary: "Get a user data", description: "Return all data from a specific user"})
|
||||
@Get(':id')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, User))
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.usersService.findOne(+id);
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'User'))
|
||||
async findOne(@Param('id') id: string, @getUser() auth: User) {
|
||||
const isAdmin = auth.isAdmin;
|
||||
|
||||
const user = await this.usersService.findOne(+id, isAdmin);
|
||||
if(!user)
|
||||
throw new NotFoundException("User not found");
|
||||
|
||||
if(auth.id != user.id && !auth.isAdmin)
|
||||
throw new ForbiddenException("You can't read other user data");
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@ApiOperation({summary: "Update a user", description: "Update user with send datas"})
|
||||
|
@ -80,8 +93,11 @@ export class UsersController {
|
|||
@ApiInternalServerErrorResponse({ description: "Internal server error" })
|
||||
@Patch(':id')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Update, User))
|
||||
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Update, 'User'))
|
||||
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto, @getUser() auth: User) {
|
||||
if(!auth.isAdmin && auth.id != id)
|
||||
throw new ForbiddenException("You can't update this user");
|
||||
|
||||
return this.usersService.update(+id, updateUserDto);
|
||||
}
|
||||
|
||||
|
@ -91,8 +107,11 @@ export class UsersController {
|
|||
@ApiInternalServerErrorResponse({ description: "Internal server error" })
|
||||
@Delete(':id')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Delete, User))
|
||||
remove(@Param('id') id: string) {
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Delete, 'User'))
|
||||
remove(@Param('id') id: string, @getUser() auth: User) {
|
||||
if(!auth.isAdmin && auth.id != id)
|
||||
throw new ForbiddenException("You can't update this user");
|
||||
|
||||
return this.usersService.remove(+id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
// import * as request from 'supertest';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
|
@ -15,10 +15,10 @@ describe('AppController (e2e)', () => {
|
|||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
// it('/ (GET)', () => {
|
||||
// return request(app.getHttpServer())
|
||||
// .get('/')
|
||||
// .expect(200)
|
||||
// .expect('Hello World!');
|
||||
// });
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue