Loopback: GET /Users always returns 401 "Authorization Required"

Created on 12 Sep 2014  路  47Comments  路  Source: strongloop/loopback

Hi,

I think I'm missing something. I'm using Loopback 2.0, and on a clean install (using yeoman generator-loopback) I can't seem to get a list of Users. It always says I need authorization, is this normal? Can't I use the User model to build my collection of users?

I can successfully create, login and logout a user. But I want to get a list of user records and I can't seem to do that. Can someone please point me in the right direction?

Most helpful comment

I just ran into this problem and couldn't agree more that this should be done via the normal ACL code in user.json.

Has another issue been created to address this?

All 47 comments

By default, the User model has ACLs to prevent methods such as find to be called from the REST API. You should relax it by configuring ACLs for the model. See http://docs.strongloop.com/display/LB/Authentication+and+authorization.

Thanks Raymond! I will try relaxing the ACLs for the model.

Solved it!
I was trying to override the User acls through the model json file, for some reason this doesn't work. The solution was to do it programmatically in the server.js file.

@jcsmesquita - Can you please show me how you did this programmatically?

Hi Shankar, sorry for the delay.

In server.js, you can add something like this:

app.models.user.settings.acls = require('./user-acls.json');

and the json file looks something like this:

[ { "principalType": "ROLE", "principalId": "$everyone", "permission": "DENY" }, { "principalType": "ROLE", "principalId": "$everyone", "permission": "ALLOW", "property": "create" }, { "principalType": "ROLE", "principalId": "admin", "permission": "ALLOW", "property": "deleteById" }, { "principalType": "ROLE", "principalId": "$everyone", "permission": "ALLOW", "property": "login" }, { "principalType": "ROLE", "principalId": "admin", "permission": "ALLOW", "property": "exists" }, { "principalType": "ROLE", "principalId": "$everyone", "permission": "ALLOW", "property": "logout" }, { "principalType": "ROLE", "principalId": "admin", "permission": "ALLOW", "property": "find" }, { "principalType": "ROLE", "principalId": "admin", "permission": "ALLOW", "property": "findById" }, { "principalType": "ROLE", "principalId": "$owner", "permission": "ALLOW", "property": "updateAttributes" }, { "principalType": "ROLE", "principalId": "$everyone", "permission": "ALLOW", "property": "confirm" }, { "principalType": "ROLE", "principalId": "admin", "permission": "ALLOW", "property": "upsert" }, { "principalType": "ROLE", "principalId": "admin", "permission": "ALLOW", "property": "updateAll" } ]

"admin" is a custom user role we have in our app.

Hope that helps...

Good, thanks

We accomplish this by using the following utility function to clear the base model ACLs:

exports.clearBaseACLs = function (ModelType, ModelConfig) {
  ModelType.settings.acls.length = 0;
  ModelConfig.acls.forEach(function (r) {
    ModelType.settings.acls.push(r);
  });
};

Usage being like this (from user.js, for example):

modelUtils.clearBaseACLs(user, require('./user.json'));

This will clear the built-in ACLs and replace them with whatever is configured in the model JSON.

@doublemarked thanks.

@gustavomick no problem. At this time I would suggest converting this into a simple mixin. The logic may remain the same.

@doublemarked could you give me an example?

@gustavomick sure, why not - it's not flawless, but it gets the job done.

1: Add this to your model.json:

  "mixins": {
    "ClearBaseAcls": true
  }

2: Drop this in common/mixins/clear-base-acls.js:

var appRoot = require('app-root-path');

module.exports = function (Model, options) {
  var configFile = options.config;
  if (!configFile) {
    // Works for 99% of cases. For others, set explicit path via options.
    configFile = '/common/models/' + slugify(Model.modelName) + '.json';
  }

  var config = appRoot.require(configFile);
  if (!config || !config.settings || !config.settings.acls) {
    console.error('ClearBaseAcls: Failed to load model config from',configFile);
    return;
  }

  Model.settings.acls.length = 0;
  config.acls.forEach(function (r) {
    Model.settings.acls.push(r);
  });
};

function slugify(name) {
  name = name.replace(/^[A-Z]+/, function (s) { return s.toLowerCase(); });
  return name.replace(/[A-Z]/g, function (s) { return '-' + s.toLowerCase(); });
}

nice. why is a mixin preferable?, instead of may be a boot script, its because migrations aspects? thank you.

Because it's config driven instead of code driven. The configuration of the acls stays near the configuration of the clearing of the acls.

And generally a mixin is cleanly reusable, but it is perhaps unlikely that this example would be reused across many models.

Hey I am not able to understand the implementation @doublemarked . I have put the code
"mixins": {
"ClearBaseAcls": true
}
in model-config.json and it gives "Error: Model not found: mixins" since I dont have any model.json. Should I create a new Model named mixins?. Any leads would be awesome as I am new to loopback. Thanks.

This belongs in your common/models/whatever-model.json, not in model-config.json.

Ohkay and after putting it there it gives ClearBaseAcls: Failed to load model config from /common/models/user.json error

Do you have any ACLs defined in your user.json file? Is your file located at /common/models/user.json? Note the code above is very straight forward:

  var config = appRoot.require(configFile);
  if (!config || !config.settings || !config.settings.acls) {
    console.error('ClearBaseAcls: Failed to load model config from',configFile);
    return;
  }

Is it possible to have a clear solution to this problem, maybe with a detailed tutorial?
It is not very clear how to get all Users... Thanks

That mixin wasn't working for me, so I tweaked it and converted it to use the ES6 features now supported in node without any CLI flags.

'use strict';

const path = require('path');
const appRoot = require('app-root-path');

function slugify(name) {
  name = name.replace(/^[A-Z]+/, s => s.toLowerCase());
  return name.replace(/[A-Z]/g, s => '-' + s.toLowerCase());
}

module.exports = (Model) => {
  const configFile = path.join('./common/models/', slugify(Model.modelName) + '.json');
  const config = appRoot.require(configFile);

  if (!config || !config.acls) {
    console.error('ClearBaseAcls: Failed to load model config from', configFile);
    return;
  }

  Model.settings.acls.length = 0;
  config.acls.forEach(r => Model.settings.acls.push(r));
};

How can I Add ACL rules at run time for all model using ACL table data?

PLease look this
https://github.com/strongloop/loopback/issues/2061

This issue shouldn't be closed.
The normal method to override ACL is by using slc loopback:acl and this does't work.

@raymondfeng

+1 for reopening this issue

+1

+1

+1... why is slc loopback:acl broken? It walks me through the prompts but doesn't seem to do anything as a result of my choices.

EDIT:: seems the error existed between keyboard and chair.. : ) I was trying to change ACL permissions on the default User model. if I had read the docs I'd have known that that isn't editable. D'oh! Apologies for the confusion.

+1

@raymondfeng I agree with @romainquellec I think this issue should be reopened. The solutions in the ticket appear to be work arounds for a design problem.

It seems that because the base user model DENYs access to find, there is now way for subclasses to grant access. In my case, adding the following acl fails to allow admin users to list our user subclass.

    {
      "accessType": "READ",
      "principalType": "ROLE",
      "principalId": "admin",
      "permission": "ALLOW",
      "property": "find"
    }

@FreakTheMighty We can open a new issue specifically for the ability to override base ACLs from sub-models.

+1 I'm running into the same issue. There should be a way to override base ACLS from sub-models.

I realized that this error is generated, because have old rules registered . I created this code to solve the problem.

var async = require("async");

module.exports = function(app) {
    var User = app.models.User;
    var Role = app.models.Role;
    var RoleMapping = app.models.RoleMapping;

    User.create([{username: 'xandeturf',email: '[email protected]',password: 'xxx'}], function (err, users) {
        User.findOne({where:{username:"xandeturf"}}, function(err, dadosUser){
            if(dadosUser){
                Role.find({where:{name: 'admin'}}, function(err, rules){
                    if(rules){
                        async.eachSeries(rules, function(role, callback){
                            role.principals.destroyAll(function(er, dados){
                                if(er){
                                    console.log(er);
                                }
                                callback();
                            });
                        },function(){
                            rules = Array.isArray(rules) ? rules[0] : rules;
                            rules.principals.create({principalType: RoleMapping.USER, principalId: dadosUser.id}, function (err, principal) {
                                if (err) return console.log(err);
                                console.log(principal);
                            });
                        });
                    }else{
                        Role.create({name: 'admin'}, function (err, role) {
                            if (err){ console.log(err); }
                            role.principals.create({principalType: RoleMapping.USER, principalId: dadosUser.id}, function (err, principal) {
                                if (err) return console.log(err);
                                console.log(principal);
                            });
                        });
                    }
                });
            }
        });
    });
}

+1 @raymondfeng Is there a new issue open for overriding base ACLs?

I just ran into this problem and couldn't agree more that this should be done via the normal ACL code in user.json.

Has another issue been created to address this?

+1

Running into this problem again in a new project after implementing @doublemarked's solution in an old one--the workaround does not work.

I just want my authenticated "admin" Role to be able to execute any and all functions on a model.

@raymondfeng please included these facts in the documentation.

@FreakTheMighty I run into the same problem,I don't know why if I have In the model-config.json the ACL, RoleMapping and Role definitions with the datasource pointed to mongodb it doesn't works but if I change the dataSource to the default memory db it works like a charm.

"ACL": {
"dataSource": "db",
"public": false
},
"RoleMapping": {
"dataSource": "db",
"public": false
},
"Role": {
"dataSource": "db",
"public": false
}

@bonemx In your case, I think it maybe a bug in loopback-connector-mongodb. If you use version 1.17.0, ACL will be OK.

this should be solved by adding
RoleMapping.settings.strictObjectIDCoercion = true;
in boot script
this may help
strongloop/loopback#3121 (comment)

For loopback 3 the way to programmatically change the acls in user, or other model, following the way of @jcsmesquita, is this:

app.loopback.User.settings.acls = require('./user-acls.json');

my solution create extend model User with name user and config file common/models/user.json
{
"name": "user",
"base": "User",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
],
"methods": {}
}

on file model-config.json
{
"User": {
"dataSource": "mongodb",
"public": false
},
"user": {
"dataSource": "mongodb",
"public": true
}
}

Your_Project_Path...\node_modules\loopback\common\modelsuser.json
just go to This user.json in node_module and change the ACL permission from Deny to ALLOW

"acls": [
{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
.
.
.
]

it works for me

{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},

the above solution only works because it allows everyone to read/write everything.
but if you have to use access control and add a custom role like 'admin' it doesn't work.
you will get "Authorization Required" on 'admin' method calls.
I fail on below.

staff.json: extension of the User model

{
    "name": "Staff",
    "base": "User",
    "idInjection": true,
    "properties": {
      "roleId": {
        "type": "number"
      }
    },
    "restrictResetPasswordTokenScope": true,
    "emailVerificationRequired": false,
    "validations": [],
    "relations": {},
    "acls": [
      {
        "principalType": "ROLE",
        "principalId": "$everyone",
        "accessType": "READ",
        "permission": "ALLOW"
      },
      {
        "principalType": "ROLE",
        "principalId": "admin",
        "accessType": "WRITE",  
        "permission": "ALLOW"
      }
    ],
    "methods": []
  }
  ```

staff.js: create or update roles during load up
``` javascript
module.exports = function(Staff) {
  Staff.on('attached', function(app) {
    var Role = app.models.Role;
    var RoleMapping = app.models.RoleMapping;
    // initialize role mapping
    Staff.find().then(function(users) {
      if(users) {
          users.forEach(user => {
              RoleMapping.findOne({
                  where: {
                      principalId: user.id
                  }
              }).then(function(principal) {
                  if(principal) {
                      principal.updateAttribute({roleId: user.roleId})
                  } else {
                      RoleMapping.create({
                          principalType: RoleMapping.USER,
                          principalId: user.id,
                          roleId: user.roleId
                      }, function(err, principal) {
                          if(err) {
                              throw err;
                          }
                      })
                  }
              })
          })
      } else {
          console.log("No staff registered");
      }
    }).catch(function(err) {
        console.log(err.message);
        throw err;
    })
  });
}

creates datastore on memory db as follows:

  "models": {
    "Staff": {
      "1": "{\"password\":\"$2a$10$3ghXiwu6LO5MKlxGRw8mCOHYZ4AK./1xarLER32W6X3ZujR1L11U6\",\"email\":\"[email protected]\",\"fullname\":\"aaa\",\"id\":1,\"roleId\":1}",
      "2": "{\"password\":\"$2a$10$7cQtWKJUUqCM3vRn4IhYp.l3m4E3bFa8bksbg8CD8WclevAUwv5Ve\",\"email\":\"[email protected]\",\"fullname\":\"bbb\",\"id\":2,\"roleId\":2}",
      "3": "{\"password\":\"$2a$10$Xa5yzxpUK.hEIRRI9TBS.OeyegVd6w/u.rSPRmSsmDcflWyGsuF3y\",\"email\":\"[email protected]\",\"fullname\":\"ccc\",\"id\":3,\"roleId\":2}"
    },
    "ACL": {},
    "RoleMapping": {
      "7": "{\"principalType\":\"USER\",\"principalId\":\"1\",\"roleId\":1,\"id\":7}",
      "8": "{\"principalType\":\"USER\",\"principalId\":\"2\",\"roleId\":2,\"id\":8}",
      "9": "{\"principalType\":\"USER\",\"principalId\":\"3\",\"roleId\":2,\"id\":9}"
    },
    "Role": {
      "1": "{\"name\":\"admin\",\"created\":\"2018-01-02T07:15:22.075Z\",\"modified\":\"2018-01-02T07:15:22.075Z\",\"id\":1}",
      "2": "{\"name\":\"member\",\"created\":\"2018-01-02T07:15:22.075Z\",\"modified\":\"2018-01-02T07:15:22.075Z\",\"id\":2}",
      "3": "{\"name\":\"guest\",\"created\":\"2018-01-02T07:15:22.075Z\",\"modified\":\"2018-01-02T07:15:22.075Z\",\"id\":3}"
    }
  }

The above set up lets users to call read api on Staff model.
but it doesn't allow write calls on Staff model - guessing it's got to do with the set up of acl in staff.json or somehow the rolemapping that i've done is not being considered for.

Anybody faced the same issue yet?
Going without access control for now to make it work.

No solution to this yet??

I had subclassed User into user, and then had to combine a few of these to work. I set my user acls to:

{
"principalType": "ROLE",
"principalId": "$everyone",
"accessType": "READ",
"permission": "ALLOW"
},

And then I used the server.js user acls method suggested by jcsmesquita. And then the mixins as suggested by noderat. Eventually something there worked, and I now have control over my acls

Just disable server.enableAuth() in your boot/root.js if you're using lbv3 when you don't need to be using authentication (primarily in dev)
In that case, if you haven't already, please separate with a dev environment and prod environment.

If you still need to access /GET users ... this sounds like a permission an admin role will have. Please follow the docs on how to create an admin role.

the acls from user.json weren't working so had to manually set it in server.js

app.start = function() {
  // ...
  app.models.user.settings.acls = require('../common/models/user.json').acls
  // ...
}

the acls from user.json weren't working so had to manually set it in server.js

app.start = function() {
  // ...
  app.models.user.settings.acls = require('../common/models/user.json').acls
  // ...
}

worked for me. Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ian-lewis-cs picture ian-lewis-cs  路  4Comments

Overdrivr picture Overdrivr  路  4Comments

ImanMh picture ImanMh  路  4Comments

futurus picture futurus  路  3Comments

devotox picture devotox  路  4Comments