Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds accessibleBy helper and deprecates toMongoQuery and accessibleRecordsPlugin #795

Merged
merged 1 commit into from
Jul 22, 2023

Conversation

stalniy
Copy link
Owner

@stalniy stalniy commented Jul 22, 2023

Before

PostSchema = new Schema({})
PostSchema.plugin(accessibleRecordsPlugin)
const Post = mongoose.model<...>('Post', Post)

Post.accessibleBy(ability, 'read')

After

PostSchema = new Schema({})
const Post = mongoose.model<...>('Post', Post)
Post.find(accessibleBy(ability, 'read').Post)

@codecov-commenter
Copy link

codecov-commenter commented Jul 22, 2023

Codecov Report

Merging #795 (f376ff8) into master (e076b31) will decrease coverage by 2.35%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##           master     #795      +/-   ##
==========================================
- Coverage   94.93%   92.59%   -2.35%     
==========================================
  Files          34        3      -31     
  Lines         731       54     -677     
  Branches      175       13     -162     
==========================================
- Hits          694       50     -644     
+ Misses         20        2      -18     
+ Partials       17        2      -15     
Impacted Files Coverage Δ
packages/casl-mongoose/src/accessible_records.ts 85.71% <100.00%> (ø)
packages/casl-mongoose/src/mongo.ts 100.00% <100.00%> (ø)

... and 31 files with indirect coverage changes

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@stalniy stalniy merged commit bd58bb2 into master Jul 22, 2023
@stalniy stalniy deleted the feat/mongoose-accessibleBy branch July 22, 2023 12:19
@soknifedev
Copy link

Why was the accessible records plugin deprecated?

@stalniy
Copy link
Owner Author

stalniy commented Jul 24, 2023

Because of many many issues related to breaking changes in mongoose types and related complexity.

I want to re-orient this library to be more MongoDB specific, so:

  1. it can be used with any MongoDB driver/library (including mongoose).
  2. I will not need to handle that mongoose issues with types because there won't be dependency on mongoose types
  3. Consistent interface between mongoose and prisma plugins. So, it's easier for people to use other ODM/ORM/whatever with casl if they have experience with either prisma or mongo package

PS: I don't like ts experience in mongoose especially when it comes to applying multiple plugins and there are other issues. That's why few years ago, I started working on alternative library that can be used instead of mongoose, lightweight alternative on top of json schema. Basically driver + super duper types + fail safe. https://github.com/stalniy/jongo/blob/main/src/example.ts . So, another aim is to reduce maintenance burden.

@soknifedev
Copy link

Oh I see. However, I think that there is something wrong with the query returned by accessibleBy function.

    { '$expr': false },

In mongoose (7.0.1) it does not work, throws an error if permission isnt granted:

Error: `$expr` must be an object

I tried the query manually in mongo shell and it worked, so I think that this is a bug related to mongoose maybe?

@stalniy
Copy link
Owner Author

stalniy commented Jul 28, 2023

I also tried this in mongo shell and it worked. Strange. Let me check.

@stalniy
Copy link
Owner Author

stalniy commented Jul 28, 2023

Yes, it's mongoose. update @casl/mongoose to use { $expr: { $eq: [0,1] } } because I couldn't find { $expr: false } in docs. despite the fact { $expr: { $const: false } } also works, I couldn't find it in docs as well (found in execution plan :) ). But { $expr: { $eq: [0,1] } } is also parsed to { $expr: { $const: false } }

I'm wondering whether MongoDB v7 does optimize this query because v4 does full collection scan according to execution plan

@stalniy
Copy link
Owner Author

stalniy commented Jul 28, 2023

Postgresql optimizes queries with WHERE 1=0 and doesn't execute the query on the data and instead immediately returns empty set

@kyrstencarlson
Copy link

kyrstencarlson commented Aug 13, 2023

migrating to the current method of model.find(accessibleBy(ability, action).model) returns { $expr: { $eq: [0,1] } } even when abilities for the specified action are provided. this is proving very difficult to debug other issues, and cannot tell if the issue is from the latest release of this package, or our own implementation.

ability
 [
  {
    action: [
      'users:read',
    ],
    subject: 'SCHEMA_NAME',
    conditions: { user: '<some user id>' }
  },
]

query
{ '$expr': { '$eq': [ 0, 1 ] }, _id: '642f7c32e10eb60812993545' }

currently using this implementation works -->

const query = model.accessibleBy(ability, action).where(match).getQuery()

return model.find(query)

please advise

@stalniy
Copy link
Owner Author

stalniy commented Aug 13, 2023

They use exactly the same functions underneath

you need to provide more context. A full fake example, otherwise I won’t be able to help

@kyrstencarlson
Copy link

They use exactly the same functions underneath

you need to provide more context. A full fake example, otherwise I won’t be able to help

here is an example for a findById search with permissions using NestJS

@Injectable()
export class Service {
  constructor(
    @InjectModel(SCHEMA_NAME, DB_Name)
    private readonly _Model: Model<MockModel>
  ) {}

  public async thisMethodWorks(_id: string, permissions: UserPermissions) {
    // creates ability for the user
    const ability = await this.createForUser(permissions);
    // ability returned
    //   {
    //   action: [ 'read' ],
    //   subject: 'SCHEMA_NAME',
    //   conditions: { user_id: '${permissions._id}' }
    // },

    // checks the model and action against the user's ability, creates a query with the match params
    const query = this._Model.accessibleBy(ability, 'read').where({ _id }).getQuery();
    // query
    // {  _id: '642f7c32e10eb60812993545' }

    const item = await this._Model.findOne(query);
    // finds item by _id

    return item;
  }

  public async doesNotWork(_id: string, permissions: UserPermissions) {
    // creates ability for the user
    const ability = await this.createForUser(permissions);
    // ability returned
    //   {
    //   action: [ 'read' ],
    //   subject: 'SCHEMA_NAME',
    //   conditions: { user_id: '${permissions._id}' }
    // },

    // checks the model and action against the user's ability, creates a query with the match params
    const query = this._Model.findOne(accessibleBy(ability, 'read')._Model).where({ _id }).getQuery();
    // query
    // { '$expr': { '$eq': [ 0, 1 ] }, _id: '642f7c32e10eb60812993545' }

    const item = await this._Model.findOne(query);
    // no item found

    return item;
  }
}

@stalniy
Copy link
Owner Author

stalniy commented Aug 13, 2023

What is accessibleBy(…)._Model and how do you expect accessibleBy to convert it to SCHEMA_NAME string literal?

@kyrstencarlson
Copy link

kyrstencarlson commented Aug 13, 2023

your docs say to do this

await Post.find(accessibleBy(ability, action).Post)

so when using class instance Im chaining the model to the accessibleBy(...) is this not what was intended?

@stalniy
Copy link
Owner Author

stalniy commented Aug 13, 2023

What you use after dot of accessibleBy is a subject type. It should be entity name which you use in your rules definition. Otherwise there is no way for cask to know what rules to convert in your case to mongo query.

That’s why you get result you get. Casl has no rules for _Model subject type.

I was thinking to add additional helper function of. So then you could do:

To fix your case, you need to do accessibleBy(…)[this._Model.modelName] but I’m not a nestjs guru)

@Alex0007
Copy link

Hi @stalniy

Any chance to make this new approach work with subjects defined as classes (with modelName) (+detectSubjectType used when defining ability)?

I do not understand how to use new accessibleBy helper function.

Lets say i have subject UserModel which has modelName: User

accessibleBy(ability, 'read').User // doesn't work

This is how internal PureAbility map looks like:

Screenshot 2023-08-18 at 12 50 07

@stalniy
Copy link
Owner Author

stalniy commented Aug 18, 2023

Currently it doesn’t work with classes. But I think it’s easily fixable. I will add method of

accessibleBy().of(User). The same will work with strings

@m-okm
Copy link

m-okm commented Dec 28, 2023

I am using nestjs (based on fastify) with mongoose and casl and was facing an issue with accessibleBy.
The return value was {$expr: {$eq:[0,1]}}
I have used a decorator to extract the user object from the request. To guarantee type safety I used the class validator with plainToInstance and validate.
The plainToInstance corrupted the MongoAbility<[Action, Subjects]>, how... I cannot tell.
Perhaps someone has an Idea why/how it happens, else it is "FYI"

@Alex0007
Copy link

Alex0007 commented Dec 28, 2023

Hi @stalniy! I lost this comment above, tried to search it several times, but no luck. I'm glad it appeared in notifications :)

Is accessibleBy().of(User) still planned? Or it's better to move away from class-based subjects?

@stalniy
Copy link
Owner Author

stalniy commented Dec 31, 2023

It’s planned. Just didn’t have time for this.

@Alex0007
Copy link

Alex0007 commented Feb 14, 2024

@stalniy maybe Model.accessibleBy() should be un-deprecated then, as there are no alternatives when subjects are classes?

I will try to make PR for accessibleBy().of(User), but i'm not sure if it is better, because it leaves space for mistake in subject, while Model.accessibleBy() determines subject automatically

@stalniy
Copy link
Owner Author

stalniy commented Feb 14, 2024

@Alex0007 I've already started doing it.

I understand your concern but I plan to get rid of mongoose plugins because of its typing complexity. Anyway, this will be easy to replicate in userland because it will be just a wrapper around accessibleBy helper.

By the way, I want to change the interface to this:

  • accessibleBy(ability).ofType(User) - works only for subject types
  • accessibleFieldsBy(ability).ofType(User) for subject types and accessibleFieldsBy(ability).of(user) - for particular document

And there won't be accessibleBy(ability).User version

@stalniy
Copy link
Owner Author

stalniy commented Feb 14, 2024

draft PR is here -> #880

Need to test accessibleFieldsBy and add corresponding tests

@alwyntan
Copy link

Hi! Love this package a lot! Just wondering hows the accessibleFieldsBy looking?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants