Loopback: Custom model not respect $owner ACL

Created on 21 Nov 2014  路  21Comments  路  Source: strongloop/loopback

I created a custom object Foo and set ACL for method updateAttributes:

{
  "principalType": "ROLE",
  "principalId": "$everyone",
  "permission": "DENY"
},
{
  "principalType": "ROLE",
  "principalId": "$owner",
  "permission": "ALLOW",
  "property": "updateAttributes"
}

After login response of API is 401 Authorization Required.
The model Role checks if the instance of model is User or subclass of user #L145 and then checks for specific property attribute #L159
In my case Model Foo extends PersistedModel and attribute to check is id.
workaround for me -> var ownerId = inst.userId || inst.owner || inst.id;

feature stale

Most helpful comment

Please note that $owner role only works with following REST api pattern:

/api/myModels/:id/...

The id will be used to look up the myModel instance. Then it will be checked if it belongs to the current user.

All 21 comments

For the $owner role to be functional, the target object needs to have a belongsTo relation to User model.

The model Foo is in substitution of built-in model User. In this case solution is to implement custom role?
My case: I have create my custom model User (extend PersistedModel) and I want to use special roles '$owner', '$related', '$authenticated', '$unauthenticated', '$everyone' with model ACL. is this possible?

I have found discussion #397 and your comment that clarify my problem. When implement possibility to redefine model User?

+1
please update if you find a solution

any more insights?

so?
Actually developing something as trivial and as simple as an app that has a users each user can have a PRIVATE (_$owner_) notebook and only he updates and reads it just doesn't seem to work, I'v searched the documents and everything followed the tutorials and I get _Unauthorized 401_ when I set my ACL INTUITIVELY to this:

  "acls": [
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "DENY"
    },
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW"
    }

Not sure if I'm missing something out, but here is a better detailed case anyway.

When I use _principalId $authenticated_ everything is perfect when it is _$owner_, I get _401_
.

_ACL and RELATIONS_
(being is the the user extension)

 "relations": {
    "being": {
      "type": "belongsTo",
      "model": "Being",
      "foreignKey": "userId"
    },
    "sections": {
      "type": "hasMany",
      "model": "Section",
      "foreignKey": "notebookId"
    }
  },
  "acls": [
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "DENY"
    },
    {
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW",
    }
  ], 

_Client side request method_
(Using the api explorer returned the same status code too)

          function getNotebook(){
            return Notebook.findOne({
              filter: {
                where: {
                  userId: AuthService.getCurrentId()
                },

              }
            })
          }

Trying the method that _fetches a hasOne relationship_ doesn't work either.

          function getNotebook(){
            return User.notebook()
          }

_The debug log_

loopback:security:role isInRole(): $everyone +35s
  loopback:security:access-context ---AccessContext--- +0ms
  loopback:security:access-context principals: +1ms
  loopback:security:access-context principal: {"type":"USER","id":"55e9c65d941d3ec208953644"} +0ms
  loopback:security:access-context modelName Being +0ms
  loopback:security:access-context modelId 55e9c65d941d3ec208953644 +0ms
  loopback:security:access-context property findById +0ms
  loopback:security:access-context method findById +0ms
  loopback:security:access-context accessType READ +0ms
  loopback:security:access-context accessToken: +0ms
  loopback:security:access-context   id "CRZ63uz0YZDFslVOSXxOti52EC8XVEeeMqOIdWRcxpxm0Phghwvx4auSFmYKDDoB" +0ms
  loopback:security:access-context   ttl 1209600 +0ms
  loopback:security:access-context getUserId() 55e9c65d941d3ec208953644 +0ms
  loopback:security:access-context isAuthenticated() true +0ms
  loopback:security:role Custom resolver found for role $everyone +0ms
  loopback:security:role isInRole(): $owner +1ms
  loopback:security:access-context ---AccessContext--- +0ms
  loopback:security:access-context principals: +0ms
  loopback:security:access-context principal: {"type":"USER","id":"55e9c65d941d3ec208953644"} +0ms
  loopback:security:access-context modelName Being +0ms
  loopback:security:access-context modelId 55e9c65d941d3ec208953644 +0ms
  loopback:security:access-context property findById +0ms
  loopback:security:access-context method findById +0ms
  loopback:security:access-context accessType READ +0ms
  loopback:security:access-context accessToken: +0ms
  loopback:security:access-context   id "CRZ63uz0YZDFslVOSXxOti52EC8XVEeeMqOIdWRcxpxm0Phghwvx4auSFmYKDDoB" +0ms
  loopback:security:access-context   ttl 1209600 +0ms
  loopback:security:access-context getUserId() 55e9c65d941d3ec208953644 +0ms
  loopback:security:access-context isAuthenticated() true +0ms
  loopback:security:role Custom resolver found for role $owner +1ms
  loopback:security:role isOwner(): Being 55e9c65d941d3ec208953644 userId: 55e9c65d941d3ec208953644 +0ms
  loopback:security:acl The following ACLs were searched:  +0ms
  loopback:security:acl ---ACL--- +0ms
  loopback:security:acl model Being +0ms
  loopback:security:acl property findById +0ms
  loopback:security:acl principalType ROLE +0ms
  loopback:security:acl principalId $owner +0ms
  loopback:security:acl accessType * +0ms
  loopback:security:acl permission ALLOW +0ms
  loopback:security:acl with score: +1ms 8016
  loopback:security:acl ---ACL--- +0ms
  loopback:security:acl model Being +0ms
  loopback:security:acl property * +0ms
  loopback:security:acl principalType ROLE +0ms
  loopback:security:acl principalId $everyone +0ms
  loopback:security:acl accessType * +0ms
  loopback:security:acl permission DENY +0ms
  loopback:security:acl with score: +0ms 7495
  loopback:security:acl ---Resolved--- +0ms
  loopback:security:access-context ---AccessRequest--- +0ms
  loopback:security:access-context  model Being +0ms
  loopback:security:access-context  property findById +0ms
  loopback:security:access-context  accessType READ +0ms
  loopback:security:access-context  permission ALLOW +0ms
  loopback:security:access-context  isWildcard() false +1ms
  loopback:security:access-context  isAllowed() true +0ms
  loopback:security:role isInRole(): $everyone +110ms
  loopback:security:access-context ---AccessContext--- +0ms
  loopback:security:access-context principals: +0ms
  loopback:security:access-context principal: {"type":"USER","id":"55e9c65d941d3ec208953644"} +0ms
  loopback:security:access-context modelName Notebook +0ms
  loopback:security:access-context modelId undefined +0ms
  loopback:security:access-context property findOne +0ms
  loopback:security:access-context method findOne +0ms
  loopback:security:access-context accessType READ +0ms
  loopback:security:access-context accessToken: +1ms
  loopback:security:access-context   id "CRZ63uz0YZDFslVOSXxOti52EC8XVEeeMqOIdWRcxpxm0Phghwvx4auSFmYKDDoB" +0ms
  loopback:security:access-context   ttl 1209600 +0ms
  loopback:security:access-context getUserId() 55e9c65d941d3ec208953644 +0ms
  loopback:security:access-context isAuthenticated() true +0ms
  loopback:security:role Custom resolver found for role $everyone +0ms
  loopback:security:role isInRole(): $owner +0ms
  loopback:security:access-context ---AccessContext--- +0ms
  loopback:security:access-context principals: +0ms
  loopback:security:access-context principal: {"type":"USER","id":"55e9c65d941d3ec208953644"} +0ms
  loopback:security:access-context modelName Notebook +0ms
  loopback:security:access-context modelId undefined +0ms
  loopback:security:access-context property findOne +0ms
  loopback:security:access-context method findOne +1ms
  loopback:security:access-context accessType READ +0ms
  loopback:security:access-context accessToken: +0ms
  loopback:security:access-context   id "CRZ63uz0YZDFslVOSXxOti52EC8XVEeeMqOIdWRcxpxm0Phghwvx4auSFmYKDDoB" +0ms
  loopback:security:access-context   ttl 1209600 +0ms
  loopback:security:access-context getUserId() 55e9c65d941d3ec208953644 +0ms
  loopback:security:access-context isAuthenticated() true +0ms
  loopback:security:role Custom resolver found for role $owner +0ms
  loopback:security:acl The following ACLs were searched:  +0ms
  loopback:security:acl ---ACL--- +0ms
  loopback:security:acl model Notebook +1ms
  loopback:security:acl property * +0ms
  loopback:security:acl principalType ROLE +0ms
  loopback:security:acl principalId $everyone +0ms
  loopback:security:acl accessType * +0ms
  loopback:security:acl permission DENY +0ms
  loopback:security:acl with score: +0ms 7495
  loopback:security:acl ---Resolved--- +0ms
  loopback:security:access-context ---AccessRequest--- +0ms
  loopback:security:access-context  model Notebook +0ms
  loopback:security:access-context  property findOne +0ms
  loopback:security:access-context  accessType READ +0ms
  loopback:security:access-context  permission DENY +0ms
  loopback:security:access-context  isWildcard() false +1ms
  loopback:security:access-context  isAllowed() false +0ms

Thanks.

For the $owner role to be functional, the target object needs to have a belongsTo relation to User model.

@raymondfeng where can I find about this in the documentation?

I'm having the same problem @upq is having. Everything seems to be alright but still gettin 401.

Please note that $owner role only works with following REST api pattern:

/api/myModels/:id/...

The id will be used to look up the myModel instance. Then it will be checked if it belongs to the current user.

@raymondfeng I wasn't passing the owner_id when creating the object with POST /api/MyModels. So the $owner role couldn't work.

Now I'm trying to figure out the best way to validate the owner_id that's passed on creation of MyModel so that a user can't add MyModel to other users.

"The id will be used to look up the myModel instance. Then it will be checked if it belongs to the current user."

@raymondfeng Is it possible to redefine method to determine the owner, for instance if I want to call method with objectName : /api/myModels/:name/ ?

Answering to myself: was easier than I initially thought.

  Customer.beforeRemote('getTokens', function (ctx,customer,next) {
    if (ctx.req.accessToken.customerid !== ctx.args['userid'] )  {
      var errreply = new Error();
      errreply.statusCode = 400;
      next(errreply);
    }
    else {
      next();
    }
  });

Note that the HTTP path of the target model's remote method _must_ use the substring :id for this to work.

This _will NOT_ work:

foo.remoteMethod('fooBar', { http: { path: '/:fooId/fooBar' } });

Whereas this _will_ work:

foo.remoteMethod('fooBar', { http: { path: '/:id/fooBar' } });

Loopback looks for an id parameter (that property name only) when doing the access control check.

https://github.com/strongloop/loopback/blob/master/lib/application.js#L350

I facing similar issue. In my case, I allow $owner to delete, but turn out everyone can delete.

I opened a same issue here according to @raymondfeng 's answer.

Please note that $owner role only works with following REST api pattern:

/api/myModels/:id/...

The id will be used to look up the myModel instance. Then it will be checked if it belongs to the >current user.

the loopback build-in acl only can check one iterms which indentitied by the id 锛宻o after login I want to get (READ) a list of iterms which belongs to current user is not possible. @raymondfeng is that right?

I have an extended class of User and the method Role.isOwner(modelClass, modelID,userID,callback) returns false. isUserClass function fails on the modelClass.prototype instanceof User. Any idea ?

@raymondfeng

For the $owner role to be functional, the target object needs to have a belongsTo relation to User model.

Does this mean if I have a extended user model (like AdminUser), $owner won't work even if I set the targeted model's relations as:

"relations": {
    "owner": {
      "type": "belongsTo",
      "model": "AdminUser",
      "foreignKey": "userId"
    },

Thanks for helping out!

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

This issue has been closed due to continued inactivity. Thank you for your understanding. If you believe this to be in error, please contact one of the code owners, listed in the CODEOWNERS file at the top-level of this repository.

I just ran into this. I've been trying to get an example of "I must be logged in to CRUD and I can only work with my data" and I have only been able to make it such that I have to be logged in. I had user1 make 3 objects and not only could user2 see them all, they could also get them individually.

If it helps anyone to ensure one user doesn't supply a different userId or to let the service do it for you so you don't have to explicitly pass it in, here's a simple drop-in check. Just add it to your models that have a belongTo user relation:

  Model.observe('before save', (ctx, next) => {
    const fkBelongToName = 'userId'; 
    const hasOwnerRelation = !!ctx.Model.definition.properties[fkBelongToName];
    if (!hasOwnerRelation) {
      return next();
    }
    const currentUserId = ctx.options.accessToken.userId;
    // Enforce current userId to prevent unauthorized changes
    const suppliedUserId = ctx.data ? ctx.data[fkBelongToName] : ctx.instance[fkBelongToName];
    if (!suppliedUserId) {
      // user id not supplied. force assign current user
      if (ctx.data) {
        ctx.data[fkBelongToName] = currentUserId;
      } else {
        ctx.instance[fkBelongToName] = currentUserId;
      }
    } else if (suppliedUserId !== currentUserId) {
      const err = new Error();
      err.statusCode = 401;
      err.message = 'Supplied userId reference does not match the current user';
      err.code = 'AUTHORIZATION_REQUIRED';
      return next(err);
    }
    return next();
  });
Was this page helpful?
0 / 5 - 0 ratings