getjerry / nest-casl Goto Github PK
View Code? Open in Web Editor NEWCasl integration for NestJS
License: MIT License
Casl integration for NestJS
License: MIT License
The function "assertAbility" on AccessService always throws a 401 Unauthorized error (hasAbility returns false). The code below is pretty much identical to the provided sample in the documentation. When I use the decorators, everything works as expected. What could cause this weird behavior?
NOT WORKING:
constructor(private readonly accessService: AccessService) {}
@UseGuards(JwtAuthGuard)
@Query(() => User, { nullable: true })
async user(@Args("id") id: string, @CaslUser() userProxy: UserProxy<AuthUser>) {
const user = await userProxy.get();
const subject = this.userService.findById(id);
this.accessService.assertAbility(user, Actions.read, subject);
return subject;
}
WORKING:
@UseGuards(JwtAuthGuard, AccessGuard)
@UseAbility(Actions.read, User, UserHook)
...
Here are the configured permissions, nothing special.
user({ user, can }) {
can(Actions.read, User, { id: user.id });
can(Actions.update, User, { id: user.id });
can(Actions.delete, User, { id: user.id });
}
How do I mock the proxies in a unit test? the examples only covers e2e tests, but unit tests don't work the same way.
I tried mocking the proxies in jest, but I can't figure out why it's not working
@ApiTags('Customers')
@ApiBearerAuth('jwt')
@UseGuards(AccessGuard)
@Controller('customers')
class CustomerController {
@ApiResponse({ type: [Customer] })
@ApiQuery({ name: 'companyId', required: false, description: 'Filter by company, only available for SuperAdmins' })
@UseAbility(Actions.read, Customer)
@Get()
async findAll(@CaslUser() userProxy: UserProxy<UserMetaObj>, @Query('companyId') companyId?: string) {
const user = await userProxy.get()
if (user.roles.includes(Role.SuperAdmin)) return this.customerService.findAll(companyId)
return this.customerService.findAll(user.companyId)
}
@ApiResponse({ type: Customer })
@ApiParam({ name: 'id', type: String })
@UseAbility(Actions.read, Customer, CustomerHook)
@Get(':id')
findOne(@CaslSubject() subjectProxy: SubjectProxy<Customer>) {
return subjectProxy.get()
}
}
test
const mockSubjectProxy = {
get: jest.fn().mockResolvedValue(mockCustomer),
} as unknown as SubjectProxy<Customer>
const mockUserProxy = {
get: jest.fn().mockResolvedValue({ id: mockUser.id, roles: [Role.Caregiver], companyId: mockEmployee.companyId }),
} as unknown as UserProxy<UserMetaObj>
describe('CustomerController', () => {
let controller: CustomerController
let service: CustomerService
beforeEach(async () => {
jest.resetAllMocks()
const module: TestingModule = await Test.createTestingModule({
imports: [
CaslModule.forRoot<Role>({
getUserFromRequest: (): UserMetaObj => ({
id: mockUser.id,
roles: [Role.Caregiver],
companyId: mockEmployee.companyId,
}),
}),
CaslModule.forFeature({ permissions: customerPermissions }),
],
controllers: [CustomerController],
providers: [
{
provide: EmployeeService,
useClass: MockEmployeeService,
},
{
provide: CustomerService,
useClass: MockCustomerService,
},
],
}).compile()
controller = module.get<CustomerController>(CustomerController)
service = module.get<CustomerService>(CustomerService)
})
describe('findAll', () => {
it('should call the service with the correct parameters', async () => {
const spy = jest.spyOn(service, 'findAll')
await controller.findAll(mockUserProxy)
expect(spy).toHaveBeenCalledWith(mockEmployee.companyId, mockEmployee.id)
})
})
describe('findOne', () => {
it('should return a customer', async () => {
const spy = jest.spyOn(mockSubjectProxy, 'get')
await controller.findOne(mockSubjectProxy)
expect(spy).toHaveBeenCalled()
})
})
})
throws the following error:
TypeError: Cannot read properties of undefined (reading 'roles')
36 | async findAll(@CaslUser() userProxy: UserProxy<UserMetaObj>, @Query('companyId') companyId?: string) {
37 | const user = await userProxy.get()
> 38 | if (user.roles.includes(Role.SuperAdmin)) return this.customerService.findAll(companyId)
| ^
39 | return this.customerService.findAll(user.companyId)
40 | }
41 |
The implementation of the library either requires coupled permissions on multiple entities or different routes.
Let's say I want to define create on Entity1 and define delete on Entity2 => I will either need to couple the definitions or separate out routes.
Offered solution:
@UseGuards(JwtAuthGuard, MultiAccessGuard)
@UseMultiAbility([
{
action: Actions.update,
subject: OrganizationEntity,
subjectHook: OrganizationHook,
},
{
action: Actions.create,
subject: OrganizationUserEntity,
},
{
action: Actions.create,
subject: OrganizationRoleEntity,
},
])
Currently using this solution in a local implementation. Would be good to have it available out of the box.
Similarly for the rest of the proxies.
For example:
a product resolver that can create related variants:
async createProduct(@Args() args: CreateOneProductArgs, @Info() info?: GraphQLResolveInfo) {
const select = new PrismaSelect(info).value
return this.productsService.createProduct({
...args,
...select,
})
}
with
{
"data": {
"sku": 'example',
"variants": {
"create": [
{
"sku": 'example-a',
"brands": 'example',
"price": 100
}
]
}
}
}
Can I use double UseAbility
decorators in this case?
@UseGuards(AuthGuard, AccessGuard)
@UseAbility(Actions.create, Product)
@UseAbility(Actions.create, ProductVariant)
I have defined following rules:
export type Subjects = InferSubjects<
typeof Product | typeof ProductVariant | typeof ProductCategory | typeof ProductType
>
...
storeManager({ can, cannot }) {
can(Actions.read, Product)
can(Actions.create, Product)
can(Actions.update, Product)
cannot(Actions.delete, Product)
can(Actions.manage, ProductVariant)
},
Hi, I just use very simple example from documentation
I have the following code
export enum Roles {
Admin = "admin",
Teacher = "teacher",
Guest = "guest"
}
Register nest-casl
in module
CaslModule.forRoot<Roles, EmployeeResponse, ExpressRequest>({
superuserRole: Roles.Admin,
getUserFromRequest: request => request.employee,
})
Entity is
@Entity({ name: "employees" })
export class EmployeeEntity {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({
type: "enum",
enum: Roles,
default: Roles.Guest
})
roles: Roles[];
}
The permisiion file is
export const permissions: Permissions<Roles, Subjects, Actions> = {
[Roles.Teacher]({ user, can }) {
can(Actions.read, EmployeeEntity, { id: user.id });
can(Actions.update, EmployeeEntity, { id: user.id });
}
};
And my controller is
@Patch(":id")
@UseGuards(AuthGuard, AccessGuard)
@UseAbility(Actions.update, EmployeeEntity)
@UsePipes(new ValidationPipe())
async update(
@Param("id") id: string,
@Body() updateEmployeeDto: UpdateEmployeeDto
): Promise<EmployeeResponse> {
const employee = await this.employeeService.update(id, updateEmployeeDto);
return this.employeeService.buildEmployeeResponse(employee);
}
And the problem is user with Guest
role can update the user with Teacher
role. Also it's about @Get(:id)
. Also any of this user can update Admin
.
How can fix it?
And the second problem is can(Actions.read, EmployeeEntity, { id: user.id });
also works for findAll
method. But I wanna allow read user only himself
I am using nest-casl library to implement authorization on my GraphQL API.
I set the permission as follows:
// customers.permission.ts
customer({ user, can }) {
can(Actions.read, Customer, { id: user.id });
}
And used the guard and ability as follows:
// customers.service.ts
@UseGuards(GqlAuthGuard, AccessGuard)
@UseAbility(Actions.read, Customer)
@Query(() => Customer, { name: 'getCustomerById' })
async getCustomerById(
@Args('id')
id: string,
) {
return await this.customersService.getCustomerById(id);
}
But unfortunately, my customer
can query other customer
s by their ID. This should not be happening, Can anyone help, please?
At the moment, only the user from the HTTP request is available in the context of ability check.
nest-casl/src/access.service.ts
Line 57 in 6a012da
Line 27 in 6a012da
Hey @liquidautumn,
Thanks for your work on this lib!
I get the following error when trying to restrict a Get User/:id route access to let the user access only his infos. FYI, restriction works fine if using a patch/put route.
Could it be caused by an empty request.body from a Get request?
"message": "Cannot convert undefined or null to object",
"stack":
at AccessService.isThereAnyFieldRestriction (C:\dev\engine-api\node_modules\nest-casl\src\access.service.ts:140:46)
at AccessService.canActivateAbility (C:\dev\engine-api\node_modules\nest-casl\src\access.service.ts:119:42)
Permission file:
export const userPermissions: Permissions<Role, Subjects, Actions> = {
user({ user, can, cannot }) {
can(Actions.read, User, { id: user.id }); // This case is causing the exception
can(Actions.update, User, { id: user.id }); // This case works fine
cannot(Actions.delete, User);
},
sentry({ extend, can }) {
extend(Role.User);
can(Actions.delete, User);
},
};
Hook file
@Injectable()
export class UserSubjectHook implements SubjectBeforeFilterHook<User, Request> {
constructor(readonly userService: UserService) {}
async run({ params }: Request): Promise<User> {
return this.userService.getById(params.id);
}
}
Problematic Controller route:
@Get(':id')
@UseAbility(Actions.read, User, UserSubjectHook)
@UsePipes(ParseUUIDPipe)
async getOne(@Param('id') id: string) {
const entity = await this.userService.getById(id);
const getUserResponse: GetUserResponse = {
id: entity.id,
email: entity.email,
roles: entity.roles,
};
return getUserResponse;
}
Hi, thanks for making this awesome module. I have a question though, in the documentation I saw there is a type InferSubject
in the code. Where does it come from? And also, what does it do?
In some cases, ie for superuser, subject could be undefined.
Use proxy, same as for user and conditions.
https://github.com/getjerry/nest-casl/blob/master/src/interfaces/authorizable-user.interface.ts#L1
As readme shows user ID is much likely to be a number, so the AuthorizableUser be:
export interface AuthorizableUser<Roles = string, Id = string | number> {
id: Id;
roles: Array<Roles>;
}
I believe that this is related to #260.
Code like the following:
can(Actions.update, Movie);
cannot(Actions.update, Movie, { status: 'PUBLISHED' });
produces the following conditions, sql, and ast:
// conditions.get()
{ status: 'PUBLISHED' }
// conditions.toSql()
[ '"status" = $1', [ 'PUBLISHED' ], [] ]
// conditions.toAst()
{ operator: 'eq', value: 'PUBLISHED', field: 'status' }
As you can see, the provided objects do not indicate that a cannot
rule was used, so it's indistinguishable in code from the can
rules.
I'd like to be able to use packRules
to share permissions with an Angular front end app.
The following hack works:
const user = userProxy?.getFromRequest();
if (user) {
const abilityFactory: AbilityFactory = (this.accessService as any)
.abilityFactory;
console.log(packRules(abilityFactory.createForUser(user).rules));
Where this.accessService
basically hijacks this private field from AccessService:
nest-casl/src/access.service.ts
Line 16 in a3af836
Exporting AbilityFactory
, or an API to get to the underlying casl
abilities would be great.
https://github.com/getjerry/nest-casl/releases/tag/v1.6.13
This PR has removed the get
method from ConditionsProxy. Is this project supposed to be following SEMVER? If so, this should have been published as a V2 release, or left the deprecated behavior in.
I would like to pass an ability builder (Prisma) and get the desired prisma query support in permission definition
You can define permissions like:
export const userPermissions: Permissions<Roles, Subjects, Actions> = {
everyone({ user, can }) {
can(Actions.read, 'User', { userId: user.id });
},
};
However, the types of the @UseAbility
decorator cannot take a string for the subject parameter so @UseAbility(Actions.read, 'User')
shows a type error.
It can be //@ts-ignore
d and everything works. It would be great to fix the type however.
As it is currently written injection of the services in the subject hook can only resolve services directly provided in the current module.
It is a common pattern to seperate out services into submodules when in a large mono-repo. For example data-access services can be place in a data-access module for import into multiple feature modules.
This is a fairly simple change and only requires adding { strict: false }
to the moduleRef.get. I will make a pr shortly.
ConditionsProxy.get currently combins all the conditions into a single object. This prevents you from declaring rules like the following.
Ex: Anyone can access public uploads, and users can access their own uploads
everyone({ can }) {
can(Actions.read, Upload, { public: true });
},
user({ can, cannot, user }) {
can(Actions.read, Upload, { user: user.id, public: false });
},
Calling conditions.get()
produces the following object:
{ user: '4663a9ea-627c-483e-8e28-88739ffa0dc0', public: true }
Is there a possible way to work around this?
The problematic code can be found here:
nest-casl/src/proxies/conditions.proxy.ts
Line 11 in 1e785bb
Thanks!
In some situations we find that a subject tuple hook is possible that only requires data from the request and does not require a service. Eg construct an subject that only has an id from the id passed into the request.
Currently we pass a dummy service that does nothing but it would be more convienent to have a SubjectTupleHook that doesn't have a service.
Users are not allowed to set how to detect subject type when they are building abilities. It may cause the wrong subject type detection and get an unpredictable result.
e.g.
it('test', async () => {
expect(accessService.hasAbility(user, Actions.delete, new Post())).toBeTruthy(); // not pass
});
The test above would not pass but can be solved by the below solutions.
ability.factory.ts
...
// For PureAbility skip conditions check, conditions will be available for filtering through @CaslConditions() param
if (abilityClass === PureAbility) {
return ability.build({ conditionsMatcher: nullConditionsMatcher, detectSubjectType: object => object.constructor as ExtractSubjectType<Subjects> });
}
return ability.build({ detectSubjectType: object => object.constructor as ExtractSubjectType<Subjects> });
or
it('test', async () => {
const post = new Post();
expect(accessService.hasAbility(user, Actions.delete, subject(post.constructor as any, post))).toBeTruthy(); // pass
});
My user's credentials are provided by AD so I have groups like "Super-Duper-Admin-User" and "Totally-Normal-Not-Admin-User" that represent "admin" and "user" groups in this application. I've gotten this package to work with these groups by something like the snippet below. I'd much rather follow the pattern in the readme. Is there a way to "alias" these super long group names to the shortened aliases?
export const permissions: Permissions<Roles, Subjects, Actions> = {
'Super-Duper-Admin-User'({ can }) {
can(Actions.manage, 'all');
},
'Totally-Normal-Not-Admin-User'({ cannot }) {
cannot(Actions.manage);
},
};
Greetings,
Thank you so much for the work on this library.
I have one quick question that I'm unable to figure out nor to find out where it is implemented in the codebase, so that I can use CASL ForbiddenError.setDefaultMessage
i.e https://casl.js.org/v5/en/api/casl-ability#forbidden-error
Could you please sched some light on this so I can have specific reason set with because
?
Thanks,
-- Daniel
Hi,
First of all, thanks for your work on this package. I prefer this over the approach currently present in the NestJS docs.
I am using nest-keycloak-connect
for authentication. With that I don't have control over the shape of the request.user
object. It turns out this type does not have a roles
key. Instead it looks something like this:
{
realm_access: {
roles: string[]
},
resource_access: {
account: {
roles: string[]
}
}
// ...
}
getUserFromRequest()
appears to have an opinion about the type of request.user
. I imagine ideally it would be a generic type with an opinion about the return type of that method as a means to transform the user.
Am I missing something? Or is there another way to achieve this?
Following these instructions did not work for me. AbilityFactory.createForUser()
would be called ahead of getUserFromRequest
and getuserHook
.
CASL supports restricting access to fields:
https://casl.js.org/v5/en/guide/restricting-fields
can('update', 'Article', ['title', 'description'], { authorId: user.id });
...
defineAbilityFor(user).can('update', 'Article', 'published'); // false
But, as I see it, there is no functionality for accessService:
https://github.com/getjerry/nest-casl/blob/master/src/access.service.ts#L18
public hasAbility(user: AuthorizableUser, action: string, subject: Subject): boolean {
It would be great to implement that a feature.
I was able to implement nest-casl relatively easily which was very nice, and made it very easy to use.
However when it comes to creating test cases similar to the CASL guide ( https://casl.js.org/v4/en/advanced/debugging-testing ) , I have had no luck. Are there any working examples provided on how to test?
In my case, I need dynamic initialization with DI
for getUserFromRequest
Can we do this?
I can try to implement CaslModule.forRootAsync
If you want to get subject from hook into a class method argument, you need to use SubjectProxy
and manually get the subject (example from docs)
@Mutation(() => Post)
@UseGuards(AuthGuard, AccessGuard)
@UseAbility(Actions.update, Post, PostHook)
async updatePost(
@Args('input') input: UpdatePostInput,
@CaslSubject() subjectProxy: SubjectProxy<Post>
) {
const post = await subjectProxy.get();
}
This can be automated through a generic pipe
import { Injectable, NotFoundException, PipeTransform } from '@nestjs/common';
import type { SubjectProxy } from 'nest-casl';
@Injectable()
export class UnwrapCaslSubjectPipe<T> implements PipeTransform<SubjectProxy<T>, Promise<T>> {
async transform(subjectProxy: SubjectProxy<T>): Promise<T> {
const subject = await subjectProxy.get();
if (!subject) {
throw new NotFoundException();
}
return subject;
}
}
And code will be like this
@Mutation(() => Post)
@UseGuards(AuthGuard, AccessGuard)
@UseAbility(Actions.update, Post, PostHook)
async updatePost(
@Args('input') input: UpdatePostInput,
@CaslSubject(UnwrapCaslSubjectPipe) post: Post,
) {
// do anything with post oject
}
Hello,
I am wondering if it supports json defined rules as I want to dynamically set the permissions via dashboard.
I don't know why this happens, what i am trying to do is to simply add the AccessGuard and UseAbility Hook example working. it seems the error comes from the UseAbility.
ERROR [ExceptionsHandler] Cannot read properties of undefined (reading 'includes') TypeError: Cannot read properties of undefined (reading 'includes') at AccessService.canActivateAbility (D:\github.com\qribtech\qrib_backend\node_modules\nest-casl\src\access.service.ts:74:37) at AccessGuard.canActivate (D:\github.com\qribtech\qrib_backend\node_modules\nest-casl\src\access.guard.ts:30:37) at processTicksAndRejections (node:internal/process/task_queues:96:5) at GuardsConsumer.tryActivate (D:\github.com\qribtech\qrib_backend\node_modules\@nestjs\core\guards\guards-consumer.js:16:17) at canActivateFn (D:\github.com\qribtech\qrib_backend\node_modules\@nestjs\core\router\router-execution-context.js:134:33) at D:\github.com\qribtech\qrib_backend\node_modules\@nestjs\core\router\router-execution-context.js:42:31 at D:\github.com\qribtech\qrib_backend\node_modules\@nestjs\core\router\router-proxy.js:9:17
Hello, first of all, it is an excellent library and also helps a lot. But, can we make the roles property to be dynamic? I mean it could take either an array of an enum or a single value enum. Cause I encountered an error when I tried to supply a single value enum to the roles property.
Then, I took a look at this line of code:
The forEach
will cause the error when roles are supplied by a single value enum. Instead we could do something like this:
if (Array.isArray()) {
user.roles?.forEach((role) => {
ability.permissionsFor(role);
});
} else {
ability.permissionsFor(user.roles);
}
Thanks.
I tried to access some fields like workspaceTeamId
type Subjects = InferSubjects<typeof User> | 'all';
export const permissions: Permissions<Role, Subjects, Actions> = {
super_admin({ can }) {
can(Actions.manage, 'all');
},
user({ user, cannot }) {
cannot(Actions.read, User, {
workspaceTeamId: { $ne: user.workspaceTeamId }, // <- Error Property 'workspaceTeamId' does not exist on type 'AuthorizableUser<Role, string>'
}).because('You can only read users in your workspace'); },
};
I got this error
Property 'workspaceTeamId' does not exist on type 'AuthorizableUser<Role, string>'
app.module
@Module({
imports: [
CaslModule.forRoot<
Role,
{ id: Types.ObjectId; roles: Role[] },
{ user: UserDocument }
>({
superuserRole: Role.SUPER_ADMIN,
getUserFromRequest: (request) => ({
id: request.user._id.toString(),
...request.user,
}),
}),
// ....
})
When running i'm having this error:
/home/axelreid/MyFiles/Fullstack Projects/zap/backend/node_modules/nest-casl/dist/access.service.js:15
const flat_1 = require("flat");
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /home/axelreid/MyFiles/Fullstack Projects/zap/backend/node_modules/flat/index.js from /home/axelreid/MyFiles/Fullstack Projects/zap/backend/node_modules/nest-casl/dist/access.service.js not supported.
Instead change the require of index.js in /home/axelreid/MyFiles/Fullstack Projects/zap/backend/node_modules/nest-casl/dist/access.service.js to a dynamic import() which is available in all CommonJS modules.
at Object.<anonymous> (/home/axelreid/MyFiles/Fullstack Projects/zap/backend/node_modules/nest-casl/dist/access.service.js:15:16)
at Object.<anonymous> (/home/axelreid/MyFiles/Fullstack Projects/zap/backend/node_modules/nest-casl/dist/casl.module.js:13:26)
at Object.<anonymous> (/home/axelreid/MyFiles/Fullstack Projects/zap/backend/node_modules/nest-casl/dist/index.js:4:21)
at Object.<anonymous> (/home/axelreid/MyFiles/Fullstack Projects/zap/backend/dist/src/app.module.js:20:21)
at Object.<anonymous> (/home/axelreid/MyFiles/Fullstack Projects/zap/backend/dist/src/main.js:8:22)
I tried the version of nest-casl
from ^1.8.8
to ^1.9.1
Tried with the node versions v18.x
and v20.x
Tried to change the nestjs related packages also
tsconfig.json
:
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"esModuleInterop": true,
"moduleResolution": "Node"
}
}
Also tried to change around the config a little bit but i couldn't find a solution
I didn't have this problem just before, when i was putting project on a server i had to play around with the package.json
but didn't change deps much, i mostly removed node_modules
and package-lock.json
and re-install many times
Hello,
i am pretty new to nestjs and i am trying to implement a RBAC with predefined roles and permissions into a project.
e.g. https://casl.js.org/v5/en/cookbook/roles-with-static-permissions
I checked mostly of the nestjs-casl repo and also accesscontrol/acl ones and i think that this one is the simplest to set up also because is structured on splitted permissions file that helps a lot to have a clean code.
I am not using GraphQL and i don't know it very well, i have understood that resolvers are in the repo because it relies on it.
Th readme was a little confusing on the part because there are resolvers but no controller, that could be found in the files.
Also i saw that you are using dto to get the schema object that is passed to casl permissions.
It's possible use this without resolvers and passing Typeorm entity class instead of dto?
I tried to play a little bit to use them changing subject type detection on ability.factory.ts
https://casl.js.org/v5/en/guide/subject-type-detection
Would make sense? or the structure of this module cannot be easily changed?
Hi, I am interested in utilizing this library, however, it's not entirely clear to me how to go about 1) integrating actions/roles/permissions fetched from a database and 2) being able to apply fine-grained abilities based on a user's role(s). As a workaround, I've forked the library and implemented my own ability factory, access service, etc, but I'd love to leverage any points of extensibility that may/may not exist. Thanks in advance!
According to https://casl.js.org/v5/en/advanced/customize-ability#custom-conditions-matcher-implementation
import {
PureAbility,
AbilityBuilder,
AbilityTuple,
MatchConditions,
AbilityClass
} from '@casl/ability';
type AppAbility = PureAbility<AbilityTuple, MatchConditions>;
const AppAbility = PureAbility as AbilityClass<AppAbility>;
const lambdaMatcher = (matchConditions: MatchConditions) => matchConditions;
export default function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder(AppAbility);
can('read', 'Article', ({ authorId }) => authorId === user.id);
can('read', 'Article', ({ status }) => ['draft', 'published'].includes(status));
return build({ conditionsMatcher: lambdaMatcher });
}
Seems like it's much more flexible than MongoQuery
matcher. However in TypeScript a lamda function cannot be passed to the third argument.
No overload matches this call.
Overload 1 of 2, '(action: string | string[], subject: typeof Team | (typeof Team)[], conditions?: MongoQuery<Team>): RuleBuilder<Ability<AbilityTuple<string, Subject>, MongoQuery<...>>>', gave the following error.
Type '({ id }: { id: any; }) => boolean' has no properties in common with type 'MongoQuery<Team>'.
Overload 2 of 2, '(action: string | string[], subject: typeof Team | (typeof Team)[], fields?: string | string[], conditions?: MongoQuery<Team>): RuleBuilder<Ability<AbilityTuple<string, Subject>, MongoQuery<...>>>', gave the following error.
Argument of type '({ id }: { id: any; }) => boolean' is not assignable to parameter of type 'string | string[]'.
Type '({ id }: { id: any; }) => boolean' is missing the following properties from type 'string[]': pop, push, concat, join, and 28 more.ts(2769)
```
It appears that getUserFromRequest
allows returning an object that doesn't contain a roles
field.
The roles
field seems to be hardcoded in other places, and is required. Here's a simple repro:
type User = { name: string };
CaslModule.forRoot<Roles, User>({
getUserFromRequest: (request) => {
return {
name: 'test',
};
},
This leads to errors further down. Ideally, the signature of getUserFromRequest
would verify that a AuthorizableUser<T>
is being returned.
When I'm using the provided example for accessing the AccessService
:
//check and throw error
// 403 when no conditions
// 404 when conditions set
this.accessService.assertAbility(user, Actions.update, post);
// return true or false
this.accessService.hasAbility(user, Actions.update, post);
These functions are returning a boolean. All methods available seem to return a boolean. However the conditions are not exposed.
For example: my user can('read', Book)
but with certain conditions.
The guards are providing me with those conditions and I can apply them to my queries. The access service does not give me those conditions.
Am I missing something here or is it not possible to access the conditions from this context.
Many thanks in advance!
Currently:
import { User } from '@entities/user.entity';
import { Injectable } from '@nestjs/common';
import { Request, SubjectBeforeFilterHook } from 'nest-casl';
import { UserService } from '../user.service';
@Injectable()
export class UserHook implements SubjectBeforeFilterHook<User, Request> {
constructor(readonly userService: UserService) {}
async run({ params }: Request) {
return this.userService.getOne(params.input.id);
}
}
This is when the id of the resource to modify is pased in the dto itself however i am using put ---> users/:id
and thus the id is not part of the dto. How can we solve this
EXPECTING:
Be able to get user info with id equal to my id only (which is saved in JWT token).
CURRENT RESULT:
I am able to get info about all users with some id.
Used Nest Js docs while creating this solution. Do appreciate your help.
type Subjects = InferSubjects<typeof User | typeof Role | 'User'> | 'all';
export type AppAbility = Ability<[Action, Subjects]>;
export class CaslAbilityFactory {
createForUser(userDataFromJWT: JwtAccessTokenInput) {
const { can, cannot, build } = new AbilityBuilder<
Ability<[Action, Subjects]>
>(Ability as AbilityClass<AppAbility>);
// TESTING THIS CASE
can(Action.Read, User, {
id: userDataFromJWT.sub,
});
return build({
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}
private hasRole(roles: unknown[], role: UserRoles): boolean {
return roles.includes(role);
}
}
export class GetUserPolicyHandler implements IPolicyHandler {
handle(ability: AppAbility) {
return ability.can(Action.Read, User);
}
}
export enum Action {
Manage = 'manage',
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
}
export interface IPolicyHandler {
handle(ability: AppAbility): boolean;
}
type PolicyHandlerCallback = (ability: AppAbility) => boolean;
export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;
@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 ctx = GqlExecutionContext.create(context);
const { user }: { user: JwtAccessTokenInput } = ctx.getContext().req;
const ability = this.caslAbilityFactory.createForUser(user);
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);
}
}
@Resolver(() => User)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Query(() => User, { name: 'user' })
@UseGuards(PoliciesGuard)
@CheckPolicies(new GetUserPolicyHandler())
@UseInterceptors(UserNotExistsByIDInterceptor)
async findOne(@Args('id', { type: () => Int }) id: number): Promise<User> {
return await this.userService.findOne(id);
}
}
So I have this permissions:
member({ user, can }) {
can(Actions.read, User, { id: user.id });
can(Actions.update, User, { id: user.id });
}
In controller:
@UseGuards(JwtAuthGuard, AccessGuard)
@UseAbility(Actions.read, User, UserHook)
@Get(':id')
Will work fine with UserHook getting the User given :id as param.
But when I do this for a getall:
@UseGuards(JwtAuthGuard, AccessGuard)
@UseAbility(Actions.read, User)
@Get()
as no hook is specified (or if I send null), It will allow access to the @get route. Is this intended behaviour?
Firstly, thanks for the nice library!
When we define a class which implements the SubjectBeforeFilterHook
as outlined in the readme, the params object that is pulled from Request
seems to have any
typed which is not ideal. Is this the intended behaviour or am I doing something wrong here?
I also inspected the example code in this repo under: https://github.com/getjerry/nest-casl/blob/master/src/__specs__/app/post/post.hook.ts#L11 and noticed the same issue.
Is there a way to have this typed?
import { Injectable } from '@nestjs/common'
import type { Answer } from '@prisma/client/my-service'
import type { SubjectBeforeFilterHook, Request } from 'nest-casl'
import type { AnswerService } from './answer.service'
Injectable()
export class AnswerHook implements SubjectBeforeFilterHook<Answer, Request> {
constructor(readonly answerService: AnswerService) {}
async run({ params }: Request): Promise<Answer> {
// params has any type.
return this.answerService.get(params.someField)
}
}
Thanks!
Noticed this while trying to add this project to a Yarn 3 PnP project. See:
Seems like that dependency should be added to package.json
.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.