onury / accesscontrol

Role and Attribute based Access Control for Node.js
https://onury.io/accesscontrol
MIT License
2.21k stars 178 forks source link

Other Possession types besides 'Own' and 'Any'? #21

Closed maximilianschmitt closed 7 years ago

maximilianschmitt commented 7 years ago

Hi!

I stumbled upon accesscontrol while looking for existing solutions to some ABAC and I really like the API!

Question

Can I have more possession types than simply "Owner" and "Any"?

Use Case

Let's say I have a market place where people can sell products and others can buy them. I could have a Product resource where:

Thanks, Max

mylastore commented 7 years ago

@maximilianschmitt Can you provide a simple example for an admin viewing the admin route?

maximilianschmitt commented 7 years ago

@mylastore I'm sorry, which admin route?

mylastore commented 7 years ago

If a normal user or guest wants to access an admin route or page how can I prevent that.

jyotman commented 7 years ago

@maximilianschmitt I think you can do this -

Roles - user, owner, customer Resource - product

ac.grant('user').readAny('product', ['title', 'description']);
ac.grant('customer').extend('user').readOwn('product', ['title', 'description', 'downloadLink']);
ac.grant('owner').extend('user').readOwn('product', ['*'])
onury commented 7 years ago

Actually, "own" and "any" covers it all.
Don't limit what "own" could stand for.

Let's grant permissions first:

(Note: I suggest you don't name a role owner. Any role can own one resource or the other. Semantically it's better when you name your roles by their purpose. e.g. user, admin, editor, etc...)

const ac = new AccessControl();

ac.grant('guest')
  .readAny('products', ['title', 'description']);

const vAttrs = ['title', 'description', 'downloadLink', 'downloadsCount'];
ac.grant('vendor')
  .extend('guest')
  .createOwn('products', vAttrs)
  .readOwn('products', vAttrs);

ac.grant('customer')
  .extend('guest')
  .readOwn('products', ['title', 'description', 'downloadLink']);

So when querying for permissions; (pls see this) Own requires you to also check for the actual possession. The possession in these cases, indicates that:

Example Route: /products/:productId

const product = getProduct(Number(req.params.productId));
// "purchased by" OR "manufactured by" —> both means own'ed
const isOwned = isPurchasedBy(req.user.id, product.id) 
        || product.vendorId === user.id;
const permission = isOwned
    ? ac.can(req.user.role).readOwn('products')
    : ac.can(req.user.role).readAny('products');

if (permission.granted) {
    // attrs will differ depending on the actual role queried
    console.log(permission.attributes);
    res.json(permission.filter(product));
}

See? Owning a resource could mean many things. It's AccessControl.js, trying to be non-opinionated as much as possible; leaving many choices to the developer.

ghost commented 6 years ago

With a manager role - the objective would be to have the manager able to see and edit all fields for a user object - but only for their team. I assume the correct method would be to apply a createAll on the user object but handle the filter to restrict just people that report to them separately/manually. Is there a better method such as creating an addition resource for each team: TeamMember

scandinave commented 6 years ago

Maybe you can use the attribut functionality to handle this use case. Add an attribute with the team name on the manager role to be able to filter access.

onury commented 6 years ago

@glenarama you could consider a teamManager role. i.e. If the user belongs to own team, teamManager can update... Check out this example.

ghost commented 6 years ago

Thanks Onury - So is it something like:

ac.grant('admin').updateAll('user') ac.grant('user').updateOwn('user') ac.grant('manager').updateOwn('teamUser')

const can = ac.can(user.role);

If(isManager(userobj)) can.updateOwn('teamUser') else can.updateOwn('user')

onury commented 6 years ago
  1. There is no updateAll() method. I guess you mean updateAny()
  2. Don't need to duplicate the same resource. Instead what you need is another role. So instead of "teamUser" resource, you need "teamManager" role.

EDIT: This depends on the app design though. If the view/route lists team member accounts, you could have a teamMemberAccount resource. Then implementation could be changed a bit...

  1. Let's name the resource "account" rather than "user" (avoid confusion since we also have a role named "user")
// grants
ac.grant('admin').updateAny('account');        // grant #1
ac.grant('user').updateOwn('account');         // grant #2
ac.grant('teamManager').updateOwn('account');  // grant #3

// checks
const isOwnAccount = user.id === targetUserId;
const isOwnTeamMemberAccount = isTeamManagerOf(user.id, targetUserId);
const permission = isOwnAccount || isOwnTeamMemberAccount
    ? ac.can(user.role).updateOwn('account')     // #2, #3
    : ac.can(user.role).updateAny('account');    // #1

That pseudo function could be like this:

function isTeamManagerOf(managingUserId, memberUserId) {
    const managedTeams = getManagedTeamsOf(managingUserId); // array of team names
    const teamsToCheck = getTeamsOf(memberUserId); // array of team names
    return teamsToCheck.some(teamName => {
        return managedTeams.indexOf(teamName) >= 0;
    });
}

... or it could directly query a database...

ghost commented 6 years ago

Gotcha - Thanks. I was imagining the use case where the attribute permissions may be different from the managers Own account vs their employee's account. Three different permission types e.g:

Admin edit anything Manager edit anything in team User edit anything except field "title"

Sorry didn't clarify this in the original question. I think i do need a seperate resource to enable that structure.