accesscontrol icon indicating copy to clipboard operation
accesscontrol copied to clipboard

getGrants of specific role

Open sm2017 opened this issue 5 years ago • 6 comments

I have use case similar to https://github.com/onury/accesscontrol/issues/31

I don't want expose all roles and action to client , just roles and actions that granted

In the following

ac.grant('user').read('profile', ['*', '!id'])
   .grant('admin').extend('user');

{
    "admin": {
        "$extend": ["user"]
    },
    "user": {
        "profile": {
            "read:any": ["*", "!id"]
        }
    }
}

If the client's role is user , the admin role is exposed too

sm2017 avatar Jan 05 '19 11:01 sm2017

This is not specific to AccessControl.js. You should never expose grants / policies to client side. (Even if you only expose the policy of the required role and no other role; it's still bad practice.)

You should handle the request and grant/deny access on server side. And if granted; filter data if needed (using permission#filter() if using this lib) and return the response. Client side should have no knowledge of your policies.

onury avatar Jan 05 '19 20:01 onury

@onury I know that I must grant/deny access on server side As I told before I have use case similar to https://github.com/onury/accesscontrol/issues/31

I want to hide/show sidebar menu items based on grants, So I want to get all grants of logged in user and create new instance of AccessControl using the grants

sm2017 avatar Jan 06 '19 05:01 sm2017

@onury Reply please

sm2017 avatar Jan 13 '19 05:01 sm2017

Currently there is no built-in way to get compiled grants with resource-attributes for a role, all at once. I'm marking this as a feature request.

For now you can use this function:

const actions = ['create', 'read', 'update', 'delete'];
const possessions = ['Any', 'Own'];
function getCompiledGrantsOf(role) {
  const compGrants = {};
  const can = ac.can(role);
  let permission;
  ac.getResources().forEach(resource => {
    actions.forEach(act => {
      possessions.forEach(pos => {
        permission = can[act + pos](resource);
        if (permission.granted) {
          compGrants[resource] = compGrants[resource] || {};
          compGrants[resource][`${act}:${pos.toLowerCase()}`] = permission.attributes.concat();
        }
      });
    });
  });
  return compGrants;
}

You can also:

  • (Recommended) Implement SSR and render menu items on server side after AC handles it (e.g. SSR with React or Next.js has built-in support for SSR)

  • Or create a new resource for such things (menu-items), setup your AC to grant it. Then fetch this from your endpoint as filtered data after AC handles it.

// Let's say menu is a resource with attributes:
//    home, profile, videos, accounts
ac.grant('guest').read('menu', ['home'])
  .grant('user').read('menu', ['home', 'profile', 'videos'])
  .grant('admin').read('menu', ['*']);

// checking and filtering:
const menuItems = ['home', 'profile', 'videos', 'accounts'];
const permission = ac.can(role).read('menu');
if (permission.granted) {
  const attrs = permission.attributes;
  // instead of permission#filter we'll use Array#filter
  if (attrs.length === 1 && attrs[0] === '*') return menuItems.concat();
  return menuItems.filter(m => attrs.indexOf(m) >= 0)
}

EDIT: filtering part above feels awkward. I'll update this later.

  • Or pre-calculate the permissions required for these menu-items. A simple example would be:
// Let's say menu items are: 
//     Home, My Profile, Edit Profile, My Videos, All Videos

// on server-side
function getGrantedMenuItemsOf(role) {
    const menuItems = ['Home'];
    if (!role) return menuItems;

    const can = ac.can(role);
    if (can.readOwn('profile').granted) menuItems.push('My Profile');
    if (can.updateOwn('profile').granted) menuItems.push('Edit Profile');
    if (can.readAny('video').granted) {
        menuItems.push('My Videos');
        menuItems.push('All Videos');
    } else  if (can.readOwn('video').granted) {
        menuItems.push('My Videos');
    }

    return menuItems;
}

// Bind this to an API endpoint and fetch from client-side. e.g.:
getGrantedMenuItemsOf(req.user.role);

Note: These are all quick thoughts. Since I have almost no knowledge of your application, possibly there might be better ways. This is a security concept so, you should carefully consider/improve/test, if you decide to implement.

onury avatar Jan 14 '19 00:01 onury

I don’t agree that you should not expose permissions. Your clients or users knows what they can do and what they can’t do anyway (because you documented this somewhere on the website).

For example, on medium anonymous users can’t leave applause and it’s clear for everybody (because you just can’t do this on ui).

So, what’s the difference whether you share permissions as JSON object or as human readable text or as UX?

If you don’t share permissions, your clients will need to duplicate or even hardcode permission logic. Later it will be very hard to change.

I consider it to be safe to share permissions with client for currently logged in user. Some time ago I wrote an article about this:

https://medium.com/dailyjs/casl-and-cancan-permissions-sharing-between-ui-and-api-5f1fa8b4bec

stalniy avatar May 31 '19 12:05 stalniy

A good restful design requires to return 403 Forbidden status response for actions which are not allowed. So, anyway it’s quite easy to understand or get a list of permissions for a particular user credentials.

Side note: I think the issue with permission sharing in this library exists because it relies on user roles and not on what user can do (e.g., create post, read post, leave comment). If you could share user actions instead of roles, it would not expose any internals of underlying permission logic

stalniy avatar May 31 '19 12:05 stalniy