mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
Set up CASL abilities
This commit is contained in:
parent
b5527305cb
commit
7745e4e460
15 changed files with 222 additions and 7 deletions
|
|
@ -4,7 +4,7 @@ export function handleResponse(response) {
|
|||
return response.text().then(text => {
|
||||
const data = text && JSON.parse(text);
|
||||
if (!response.ok) {
|
||||
if ([401, 403].indexOf(response.status) !== -1) {
|
||||
if ([401].indexOf(response.status) !== -1) {
|
||||
// auto logout if 401 Unauthorized or 403 Forbidden response returned from api
|
||||
authenticationService.logout();
|
||||
// location.reload(true);
|
||||
|
|
|
|||
82
server/package-lock.json
generated
82
server/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
|||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@casl/ability": "^5.3.1",
|
||||
"@elastic/elasticsearch": "^7.13.0",
|
||||
"@google-cloud/firestore": "^4.13.1",
|
||||
"@nestjs/common": "^8.0.0",
|
||||
|
|
@ -1214,6 +1215,17 @@
|
|||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@casl/ability": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-5.3.1.tgz",
|
||||
"integrity": "sha512-wke2zAJDBKs1VtYPNDz06KyvqOgU0PBLd9kmHpGC9YLxDt9J749riX/8Vc42hxod92m7BENhOW5Yp73ICYVUbg==",
|
||||
"dependencies": {
|
||||
"@ucast/mongo2js": "^1.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/stalniy/casl/blob/master/BACKERS.md"
|
||||
}
|
||||
},
|
||||
"node_modules/@elastic/elasticsearch": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-7.13.0.tgz",
|
||||
|
|
@ -3099,6 +3111,37 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/core": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.1.tgz",
|
||||
"integrity": "sha512-sXKbvQiagjFh2JCpaHUa64P4UdJbOxYeC5xiZFn8y6iYdb0WkismduE+RmiJrIjw/eLDYmIEXiQeIYYowmkcAw=="
|
||||
},
|
||||
"node_modules/@ucast/js": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.2.tgz",
|
||||
"integrity": "sha512-zxNkdIPVvqJjHI7D/iK8Aai1+59yqU+N7bpHFodVmiTN7ukeNiGGpNmmSjQgsUw7eNcEBnPrZHNzp5UBxwmaPw==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/mongo": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.2.tgz",
|
||||
"integrity": "sha512-/zH1TdBJlYGKKD+Wh0oyD+aBvDSWrwHcD8b4tUL9UgHLhzHtkEnMVFuxbw3SRIRsAa01wmy06+LWt+WoZdj1Bw==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/mongo2js": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.3.tgz",
|
||||
"integrity": "sha512-sBPtMUYg+hRnYeVYKL+ATm8FaRPdlU9PijMhGYKgsPGjV9J4Ks41ytIjGayvKUnBOEhiCaKUUnY4qPeifdqATw==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.6.1",
|
||||
"@ucast/js": "^3.0.0",
|
||||
"@ucast/mongo": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
||||
|
|
@ -13905,6 +13948,14 @@
|
|||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"@casl/ability": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-5.3.1.tgz",
|
||||
"integrity": "sha512-wke2zAJDBKs1VtYPNDz06KyvqOgU0PBLd9kmHpGC9YLxDt9J749riX/8Vc42hxod92m7BENhOW5Yp73ICYVUbg==",
|
||||
"requires": {
|
||||
"@ucast/mongo2js": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"@elastic/elasticsearch": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-7.13.0.tgz",
|
||||
|
|
@ -15415,6 +15466,37 @@
|
|||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@ucast/core": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.1.tgz",
|
||||
"integrity": "sha512-sXKbvQiagjFh2JCpaHUa64P4UdJbOxYeC5xiZFn8y6iYdb0WkismduE+RmiJrIjw/eLDYmIEXiQeIYYowmkcAw=="
|
||||
},
|
||||
"@ucast/js": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.2.tgz",
|
||||
"integrity": "sha512-zxNkdIPVvqJjHI7D/iK8Aai1+59yqU+N7bpHFodVmiTN7ukeNiGGpNmmSjQgsUw7eNcEBnPrZHNzp5UBxwmaPw==",
|
||||
"requires": {
|
||||
"@ucast/core": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@ucast/mongo": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.2.tgz",
|
||||
"integrity": "sha512-/zH1TdBJlYGKKD+Wh0oyD+aBvDSWrwHcD8b4tUL9UgHLhzHtkEnMVFuxbw3SRIRsAa01wmy06+LWt+WoZdj1Bw==",
|
||||
"requires": {
|
||||
"@ucast/core": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"@ucast/mongo2js": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.3.tgz",
|
||||
"integrity": "sha512-sBPtMUYg+hRnYeVYKL+ATm8FaRPdlU9PijMhGYKgsPGjV9J4Ks41ytIjGayvKUnBOEhiCaKUUnY4qPeifdqATw==",
|
||||
"requires": {
|
||||
"@ucast/core": "^1.6.1",
|
||||
"@ucast/js": "^3.0.0",
|
||||
"@ucast/mongo": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js --config ormconfig.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@casl/ability": "^5.3.1",
|
||||
"@elastic/elasticsearch": "^7.13.0",
|
||||
"@google-cloud/firestore": "^4.13.1",
|
||||
"@nestjs/common": "^8.0.0",
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ import { DataSourcesModule } from './modules/data_sources/data_sources.module';
|
|||
import { OrganizationsModule } from './modules/organizations/organizations.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import ormconfig from '../ormconfig';
|
||||
import { CaslModule } from './modules/casl/casl.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: [`../.env.${process.env.NODE_ENV}`, '../.env'],
|
||||
envFilePath: [`../.env.${process.env.NODE_ENV}`, '../.env']
|
||||
}),
|
||||
TypeOrmModule.forRoot(ormconfig),
|
||||
AuthModule,
|
||||
|
|
@ -29,6 +30,7 @@ import ormconfig from '../ormconfig';
|
|||
DataQueriesModule,
|
||||
DataSourcesModule,
|
||||
OrganizationsModule,
|
||||
CaslModule
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@ import { Controller, Get, Param, Post, Query, Request, UseGuards } from '@nestjs
|
|||
import { OrganizationUsersService } from 'src/services/organization_users.service';
|
||||
import { decamelizeKeys } from 'humps';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
import { AppAbility, CaslAbilityFactory } from 'src/modules/casl/casl-ability.factory';
|
||||
import { PoliciesGuard } from 'src/modules/casl/policies.guard';
|
||||
import { CheckPolicies } from 'src/modules/casl/check_policies.decorator';
|
||||
import { User } from 'src/entities/user.entity';
|
||||
|
||||
@Controller('organization_users')
|
||||
export class OrganizationUsersController {
|
||||
constructor(
|
||||
private organizationUsersService: OrganizationUsersService
|
||||
private organizationUsersService: OrganizationUsersService,
|
||||
private caslAbilityFactory: CaslAbilityFactory
|
||||
) { }
|
||||
|
||||
// Endpoint for inviting new organization users
|
||||
|
|
@ -24,7 +29,8 @@ export class OrganizationUsersController {
|
|||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('changeRole', User))
|
||||
@Post(':id/change_role')
|
||||
async changeRole(@Request() req, @Param() params) {
|
||||
const result = await this.organizationUsersService.changeRole(req.user, params.id, req.body.role);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { Controller, Get, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { OrganizationsService } from '@services/organizations.service';
|
||||
import { decamelizeKeys } from 'humps';
|
||||
import { CaslAbilityFactory } from 'src/modules/casl/casl-ability.factory';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
|
||||
@Controller('organizations')
|
||||
export class OrganizationsController {
|
||||
constructor(
|
||||
private organizationsService: OrganizationsService
|
||||
private organizationsService: OrganizationsService,
|
||||
) { }
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, BeforeInsert, BeforeUpdate, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, BeforeInsert, BeforeUpdate, OneToMany, ManyToOne, JoinColumn, AfterLoad } from 'typeorm';
|
||||
import { Organization } from './organization.entity';
|
||||
const bcrypt = require('bcrypt');
|
||||
import { OrganizationUser } from './organization_user.entity';
|
||||
|
|
@ -48,4 +48,11 @@ export class User {
|
|||
@JoinColumn({ name: "organization_id" })
|
||||
organization: Organization;
|
||||
|
||||
public isAdmin;
|
||||
|
||||
@AfterLoad()
|
||||
generateCount(): void {
|
||||
this.isAdmin = this.organizationUsers[0].role === 'admin';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
7
server/src/modules/casl/casl-ability.factory.spec.ts
Normal file
7
server/src/modules/casl/casl-ability.factory.spec.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { CaslAbilityFactory } from './casl-ability.factory';
|
||||
|
||||
describe('CaslAbilityFactory', () => {
|
||||
it('should be defined', () => {
|
||||
expect(new CaslAbilityFactory()).toBeDefined();
|
||||
});
|
||||
});
|
||||
37
server/src/modules/casl/casl-ability.factory.ts
Normal file
37
server/src/modules/casl/casl-ability.factory.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { User } from 'src/entities/user.entity';
|
||||
import { OrganizationUser } from 'src/entities/organization_user.entity';
|
||||
import { InferSubjects, AbilityBuilder, Ability, AbilityClass, ExtractSubjectType } from '@casl/ability';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OrganizationUsersService } from '@services/organization_users.service';
|
||||
|
||||
type Actions = 'changeRole' | 'archiveUser';
|
||||
|
||||
type Subjects = InferSubjects<typeof OrganizationUser| typeof User> | 'all';
|
||||
|
||||
export type AppAbility = Ability<[Actions, Subjects]>;
|
||||
|
||||
@Injectable()
|
||||
export class CaslAbilityFactory {
|
||||
|
||||
constructor(
|
||||
private organizationUsersService: OrganizationUsersService,
|
||||
) { }
|
||||
|
||||
async archiveOrganizationUser(user: User, params: any) {
|
||||
const { can, cannot, build } = new AbilityBuilder<
|
||||
Ability<[Actions, Subjects]>
|
||||
>(Ability as AbilityClass<AppAbility>);
|
||||
|
||||
const organizationUser = await this.organizationUsersService.findOne(params.id);
|
||||
const currentUserBelongsToSameOrg = organizationUser.organizationId === user.organizationId;
|
||||
|
||||
if(user.isAdmin && currentUserBelongsToSameOrg) {
|
||||
can('changeRole', User);
|
||||
can('archiveUser', User);
|
||||
}
|
||||
|
||||
return build({
|
||||
detectSubjectType: item => item.constructor as ExtractSubjectType<Subjects>
|
||||
});
|
||||
}
|
||||
}
|
||||
16
server/src/modules/casl/casl.module.ts
Normal file
16
server/src/modules/casl/casl.module.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { OrganizationUsersService } from '@services/organization_users.service';
|
||||
import { UsersService } from '@services/users.service';
|
||||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { OrganizationUser } from 'src/entities/organization_user.entity';
|
||||
import { User } from 'src/entities/user.entity';
|
||||
import { CaslAbilityFactory } from './casl-ability.factory';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User, Organization, OrganizationUser])],
|
||||
providers: [CaslAbilityFactory, OrganizationUsersService, UsersService],
|
||||
exports: [CaslAbilityFactory]
|
||||
})
|
||||
|
||||
export class CaslModule { }
|
||||
6
server/src/modules/casl/check_policies.decorator.ts
Normal file
6
server/src/modules/casl/check_policies.decorator.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { SetMetadata } from '@nestjs/common';
|
||||
import { PolicyHandler } from './policyhandler.interface';
|
||||
|
||||
export const CHECK_POLICIES_KEY = 'check_policy';
|
||||
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
|
||||
SetMetadata(CHECK_POLICIES_KEY, handlers);
|
||||
36
server/src/modules/casl/policies.guard.ts
Normal file
36
server/src/modules/casl/policies.guard.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AppAbility, CaslAbilityFactory } from './casl-ability.factory';
|
||||
import { CHECK_POLICIES_KEY } from './check_policies.decorator';
|
||||
import { PolicyHandler } from './policyhandler.interface';
|
||||
|
||||
@Injectable()
|
||||
export class PoliciesGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private caslAbilityFactory: CaslAbilityFactory,
|
||||
) { }
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const policyHandlers =
|
||||
this.reflector.get<PolicyHandler[]>(
|
||||
CHECK_POLICIES_KEY,
|
||||
context.getHandler(),
|
||||
) || [];
|
||||
|
||||
const { user, params } = context.switchToHttp().getRequest();
|
||||
|
||||
const ability = await this.caslAbilityFactory.archiveOrganizationUser(user, params);
|
||||
|
||||
return policyHandlers.every((handler) =>
|
||||
this.execPolicyHandler(handler, ability),
|
||||
);
|
||||
}
|
||||
|
||||
private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
|
||||
if (typeof handler === 'function') {
|
||||
return handler(ability);
|
||||
}
|
||||
return handler.handle(ability);
|
||||
}
|
||||
}
|
||||
9
server/src/modules/casl/policyhandler.interface.ts
Normal file
9
server/src/modules/casl/policyhandler.interface.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { AppAbility } from '../casl/casl-ability.factory';
|
||||
|
||||
interface IPolicyHandler {
|
||||
handle(ability: AppAbility): boolean;
|
||||
}
|
||||
|
||||
type PolicyHandlerCallback = (ability: AppAbility) => boolean;
|
||||
|
||||
export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;
|
||||
|
|
@ -8,9 +8,10 @@ import { OrganizationUsersService } from '@services/organization_users.service';
|
|||
import { OrganizationsController } from '@controllers/organizations.controller';
|
||||
import { OrganizationUsersController } from '@controllers/organization_users.controller';
|
||||
import { UsersService } from 'src/services/users.service';
|
||||
import { CaslModule } from '../casl/casl.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Organization, OrganizationUser, User])],
|
||||
imports: [TypeOrmModule.forFeature([Organization, OrganizationUser, User]), CaslModule],
|
||||
providers: [OrganizationsService, OrganizationUsersService, UsersService],
|
||||
controllers: [OrganizationsController, OrganizationUsersController],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ export class OrganizationUsersService {
|
|||
private usersService: UsersService,
|
||||
) { }
|
||||
|
||||
async findOne(id: string): Promise<OrganizationUser> {
|
||||
return await this.organizationUsersRepository.findOne(id);
|
||||
}
|
||||
|
||||
async inviteNewUser(currentUser: User, params: any): Promise<OrganizationUser> {
|
||||
|
||||
const userParams = <User> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue