trailsjs / sails-permissions

Comprehensive user permissions and entitlements system for sails.js and Waterline. Supports user authentication with passport.js, role-based permissioning, object ownership, and row-level security.
MIT License
418 stars 113 forks source link

row level security #45

Open phishy opened 9 years ago

phishy commented 9 years ago

The README mentions row-level security. Is there any documentation for how this works?

maheshchari commented 9 years ago

:+1:

jonasho commented 9 years ago

1f44d

dottodot commented 9 years ago

+1

phibya commented 9 years ago

+1

satyadeepk commented 9 years ago

I see that it is not implemented yet, https://github.com/tjwebb/sails-permissions/blob/master/api/models/Permission.js#L27

tjwebb commented 9 years ago

@phishy this is now supported as of #69. Any help testing it out is appreciated.

dottodot commented 9 years ago

I would be happy to test but do you have any info on how to use it?

zxshinxz commented 9 years ago

this is awesome!

dottodot commented 9 years ago

Ok just trying the permission criteria but it doesn't seem to have any effect.

I have set the permission using

PermissionService.grant({
    role: 'public',
    model: 'Product',
    action: 'read',
    criteria: {
      blacklist: ['inventory']
    }
  });

where inventory is an association to a model

I was hoping that any public user would then not see the inventory attribute which is populated in my controller.

I've also tried it on another field that isn't an association and the attribute is still displayed in the response.

ryanwilliamquinn commented 9 years ago

@dottodot Are you using a blueprint controller or a custom controller for the Product/read request? Blacklist filtering only works if the result is sent back with res.ok, so if you are using a custom controller, make sure you send the response via res.ok.

I have also noticed some unusual behavior with the 'public' role. I will see if I can reproduce this when I have a few minutes.

dottodot commented 9 years ago

Yes I'm using a custom controller, I've just changed it to res.ok and that's made no difference and I've tried on the registered role and it's still the same.

I'm wondering if the 'where' option is required for it to work but not sure what you'd use field that can be any value.

ryanwilliamquinn commented 9 years ago

No, the 'where' option should not be required for it to work. Let me have a look at it now.

ryanwilliamquinn commented 9 years ago

Have you checked that the Permission looks correct in sails console, via Permission.find()?

dottodot commented 9 years ago

OK below is the output of Permission.find() but I'm not sure what to look for to know if it's right.

{ _context: 
   { connections: { mongo_development: [Object] },
     waterline: 
      { _collections: [Object],
        _connections: {},
        collections: [Object],
        connections: [Object],
        schema: [Object] },
     adapter: 
      { connections: [Object],
        dictionary: [Object],
        query: [Circular],
        collection: 'permission',
        identity: 'permission' },
     _attributes: 
      { model: [Object],
        action: [Object],
        relation: [Object],
        role: [Object],
        user: [Object],
        criteria: [Object],
        id: [Object],
        createdAt: [Object],
        updatedAt: [Object] },
     defaults: 
      { migrate: 'safe',
        schema: true,
        connection: 'mongo_development' },
     _cast: { _types: [Object] },
     _schema: 
      { context: [Circular],
        schema: [Object],
        hasSchema: true },
     _validator: 
      { validations: [Object],
        reservedProperties: [Object] },
     _callbacks: 
      { beforeValidate: [Object],
        afterValidate: [Object],
        beforeUpdate: [Object],
        afterUpdate: [Object],
        beforeCreate: [Object],
        afterCreate: [Object],
        beforeDestroy: [Object],
        afterDestroy: [Object] },
     _instanceMethods: {},
     hasSchema: true,
     migrate: 'safe',
     _model: [Function: bound],
     primaryKey: 'id',
     _transformer: { _transformations: {} },
     adapterDictionary: 
      { pkFormat: 'mongo_development',
        syncable: 'mongo_development',
        defaults: 'mongo_development',
        registerConnection: 'mongo_development',
        teardown: 'mongo_development',
        describe: 'mongo_development',
        define: 'mongo_development',
        drop: 'mongo_development',
        native: 'mongo_development',
        mongo: 'mongo_development',
        create: 'mongo_development',
        createEach: 'mongo_development',
        find: 'mongo_development',
        update: 'mongo_development',
        destroy: 'mongo_development',
        count: 'mongo_development',
        join: 'mongo_development',
        stream: 'mongo_development',
        identity: 'mongo_development' },
     pkFormat: 'string',
     syncable: true,
     registerConnection: [Function: bound],
     teardown: [Function: bound],
     define: [Function: bound],
     native: [Function: bound],
     mongo: { objectId: [Function] },
     findOneByAction: [Function: bound],
     findOneByActionIn: [Function: bound],
     findOneByActionLike: [Function: bound],
     findByAction: [Function: bound],
     findByActionIn: [Function: bound],
     findByActionLike: [Function: bound],
     countByAction: [Function: bound],
     countByActionIn: [Function: bound],
     countByActionLike: [Function: bound],
     actionStartsWith: [Function: bound],
     actionContains: [Function: bound],
     actionEndsWith: [Function: bound],
     findOneByRelation: [Function: bound],
     findOneByRelationIn: [Function: bound],
     findOneByRelationLike: [Function: bound],
     findByRelation: [Function: bound],
     findByRelationIn: [Function: bound],
     findByRelationLike: [Function: bound],
     countByRelation: [Function: bound],
     countByRelationIn: [Function: bound],
     countByRelationLike: [Function: bound],
     relationStartsWith: [Function: bound],
     relationContains: [Function: bound],
     relationEndsWith: [Function: bound],
     findOneById: [Function: bound],
     findOneByIdIn: [Function: bound],
     findOneByIdLike: [Function: bound],
     findById: [Function: bound],
     findByIdIn: [Function: bound],
     findByIdLike: [Function: bound],
     countById: [Function: bound],
     countByIdIn: [Function: bound],
     countByIdLike: [Function: bound],
     idStartsWith: [Function: bound],
     idContains: [Function: bound],
     idEndsWith: [Function: bound],
     findOneByCreatedAt: [Function: bound],
     findOneByCreatedAtIn: [Function: bound],
     findOneByCreatedAtLike: [Function: bound],
     findByCreatedAt: [Function: bound],
     findByCreatedAtIn: [Function: bound],
     findByCreatedAtLike: [Function: bound],
     countByCreatedAt: [Function: bound],
     countByCreatedAtIn: [Function: bound],
     countByCreatedAtLike: [Function: bound],
     createdAtStartsWith: [Function: bound],
     createdAtContains: [Function: bound],
     createdAtEndsWith: [Function: bound],
     findOneByUpdatedAt: [Function: bound],
     findOneByUpdatedAtIn: [Function: bound],
     findOneByUpdatedAtLike: [Function: bound],
     findByUpdatedAt: [Function: bound],
     findByUpdatedAtIn: [Function: bound],
     findByUpdatedAtLike: [Function: bound],
     countByUpdatedAt: [Function: bound],
     countByUpdatedAtIn: [Function: bound],
     countByUpdatedAtLike: [Function: bound],
     updatedAtStartsWith: [Function: bound],
     updatedAtContains: [Function: bound],
     updatedAtEndsWith: [Function: bound],
     definition: 
      { model: [Object],
        action: [Object],
        relation: [Object],
        role: [Object],
        user: [Object],
        id: [Object],
        createdAt: [Object],
        updatedAt: [Object] },
     meta: { junctionTable: false },
     alter: [Function: bound],
     buildDynamicFinders: [Function: bound],
     constructor: [Function: bound],
     contains: [Function: bound],
     count: [Function: bound],
     create: [Function: bound],
     createEach: [Function: bound],
     describe: [Function: bound],
     destroy: [Function: bound],
     drop: [Function: bound],
     endsWith: [Function: bound],
     find: [Function: bound],
     findAll: [Function: bound],
     findLike: [Function: bound],
     findOne: [Function: bound],
     findOneLike: [Function: bound],
     findOrCreate: [Function: bound],
     findOrCreateEach: [Function: bound],
     generateAssociationFinders: [Function: bound],
     generateDynamicFinder: [Function: bound],
     join: [Function: bound],
     select: [Function: bound],
     startsWith: [Function: bound],
     stream: [Function: bound],
     sync: [Function: bound],
     update: [Function: bound],
     validate: [Function: bound],
     where: [Function: bound],
     associations: 
      [ [Object],
        [Object],
        [Object],
        [Object] ],
     broadcast: [Function],
     getAllContexts: [Function],
     message: [Function],
     publish: [Function: bound],
     pluralize: [Function],
     room: [Function: bound],
     classRoom: [Function],
     _classRoom: [Function],
     subscribers: [Function],
     watchers: [Function],
     subscribe: [Function: bound],
     unsubscribe: [Function: bound],
     publishUpdate: [Function: bound],
     publishDestroy: [Function: bound],
     publishAdd: [Function: bound],
     publishRemove: [Function: bound],
     publishCreate: [Function: bound],
     watch: [Function: bound],
     unwatch: [Function: bound],
     introduce: [Function: bound],
     retire: [Function: bound],
     autosubscribe: true },
  _method: [Function: bound],
  _criteria: { where: null },
  _values: null,
  _deferred: null }
ryanwilliamquinn commented 9 years ago

That method returns a promise, which is what you are seeing there. Try this: Role.find({name: 'public'}).exec(console.log) Take the id from the role and plug it into this query: Permission.find({role: theIdFromRole, action: 'read'}).exec(console.log)

ryanwilliamquinn commented 9 years ago

Actually the second query should look like this: Permission.find({role: theIdFromRole, action: 'read'}).populate('criteria').exec(console.log)

dottodot commented 9 years ago

So this is the response...

[
    {
        criteria: [
            {
                blacklist: [
                    'inventory'
                ],
                permission: '55a7b4efeed630c81a14e659',
                createdAt: '2015-07-16T13: 43: 11.118Z',
                updatedAt: '2015-07-16T13: 43: 11.118Z',
                id: '55a7b4efeed630c81a14e65a'
            }
        ],
        model: '55a7b4c0a2e56fae1a48666b',
        role: '55a7b4c0a2e56fae1a486679',
        action: 'read',
        relation: 'role',
        createdAt: '2015-07-16T13: 43: 11.111Z',
        updatedAt: '2015-07-16T13: 43: 11.114Z',
        id: '55a7b4efeed630c81a14e659'
    }
]
ryanwilliamquinn commented 9 years ago

That looks good. When you tried on the registered role, did you make the request with a registered (logged in) user?

dottodot commented 9 years ago

Yes I logged in as the registered user then made the request.

But just found the cause. It was a combination of me missing adding the Criteria Policy to policy.js (sorry about that) and then I was also sending my response as below for pagination purposes.

res.ok({
            items: products,
            totalRecords: count
          });

It would seem the criteria policy will only work if sent as

res.ok(products)

Is there any way around this? Or is it just a case of me creating a custom Criteria Policy to meet my needs?

ryanwilliamquinn commented 9 years ago

Sorry I didn't respond to this earlier. You are correct, the 'read' filtering only works if you send it like res.ok(products) The code that does it is here: https://github.com/tjwebb/sails-permissions/blob/master/api/policies/CriteriaPolicy.js#L84

I will keep thinking about how to extend the read attribute filtering.

ryanwilliamquinn commented 9 years ago

@phishy there is a bit of documentation at the bottom of this page about row-level security: https://github.com/tjwebb/sails-permissions/wiki/Managing-Roles-and-Permissions

khchan commented 8 years ago

@dottodot Did you ever manage to find a solution for this? I'm having the same problem.

dottodot commented 8 years ago

@khchan Sorry which problem are you referring as I mentioned a couple I was having.

khchan commented 8 years ago

@dottodot ah should have been more specific. I was wondering if you found a solution for res.ok to have a pagination total or did you end up having to create a custom criteria policy?

dottodot commented 8 years ago

@khchan I did it by sending the count in a header like this.

User.count().then(function(count) {
      User.find().paginate({
        page: req.param('page'),
        limit: req.param('limit')
      }).then(function(users) {
        res.set('Access-Control-Expose-Headers', 'X-Total-Count');
        res.set('X-Total-Count', count);
        res.ok(users);
      });
    });