Loopback: Question/s: ACL in DB (mysql)

Created on 6 Oct 2016  路  7Comments  路  Source: strongloop/loopback

I'm new in Loopback and I'm trying to do things one step at a time.
Before I put my ACL in DB, my ACL definition in JSON file of the models works.

When I put the ACL in DB (mysql), it's not working. I think the ACL for the model base User ('Owner' in my code) loads the default ACL for User model and the model base PersistedModel ('Task' in my code) do not have any ACL.

Did I forget to set anything? Please help.

Further Details:
My models datasource are all "mysql".
_server/model-config.json_

...
"AccessToken": {
    "dataSource": "mysql",
    "public": false
  },
  "ACL": {
    "dataSource": "mysql",
    "public": false
  },
  "RoleMapping": {
    "dataSource": "mysql",
    "public": false
  },
  "Role": {
    "dataSource": "mysql",
    "public": false
  },
  "Task": {
    "dataSource": "mysql",
    "public": true
  },
  "Owner": {
    "dataSource": "mysql",
    "public": true
  }

Below is the definition of my server/datasource.json
_server/datasource.json_

{
  "db": {
    "name": "db",
    "connector": "memory"
  },
  "mysql": {
    "host": "localhost",
    "port": 3306,
    "url": "",
    "database": "loopback_tasks",
    "password": "qwertyuiop",
    "name": "mysql",
    "user": "loopback",
    "connector": "mysql"
  },
  "mongo":{
    "host": "localhost",
    "name": "",
    "password": "",
    "port": 27017,
    "database":"loopback_tasks",
    "connector": "mongodb"
  }
}

I execute this script separately before running the API server. This creates the built-in model to datasource.
_server/bin/create-lb-tables.js_

var path = require('path');

var server = require(path.resolve(__dirname, '../server'));
var my = server.dataSources.mysql;
var mo = server.dataSources.mongo;

var lbTables = ['Owner','Task','AccessToken','ACL','RoleMapping','Role'];
my.automigrate(lbTables,function(er){
    if (er) throw er;
    console.log('Loopback tables [' + lbTables + '] created in '+my.adapter.name);
    my.disconnect();
    process.exit(0);
});

I also execute this script separately before running the API server. This creates the initial data in db.
_server/bin/sample-data.js_

var path = require('path');
var async = require('async');

var app = require(path.resolve(__dirname, '../server'));
var User = app.models.Owner;
var Task = app.models.Task;
var Role = app.models.Role;
var RoleMapping = app.models.RoleMapping;
var ACL = app.models.ACL;

User.create(
    [
        {email: "[email protected]", password: "qwerty"},
        {email: "[email protected]", password: "qwerty"},
    ],
    function(err, owners){
        if (err) throw err;

        console.log("Owners created: ",owners);

        async.parallel({
            role:   async.apply(createRole),
            tasks:  async.apply(createTask),
            acls:   async.apply(createACLs),
        }, function(err, results){
            if (err) throw err;

            console.log('Done!');
            process.exit(0);
        });

        function createRole(cb){
            Role.create({
                    name: 'admin'
                }, function(err, role) {
                    if (err) return cb(err);
                        console.log(role);

                        // Make Bob an admin
                        role.principals.create({
                            principalType: RoleMapping.USER,
                            principalId: owners[0].id
                        }, function(err, principal) {
                            if (err) return cb(err);
                            console.log(principal);

                            return cb(null, true);
                    });
                });
        }

        function createTask(cb){
            Task.create(
                [
                    {
                        task: "Arf",
                        description: "Arf arrrffff arf",
                        ownerId : owners[1].id
                    },
                    {
                        task: "Aww",
                        description: "Aww aww aw",
                        ownerId : owners[1].id
                    }
                ], function(err, task){
                    if (err) return cb(err);

                    console.log("Task created: ",task);

                    return cb(null, true);
                    //process.exit(0);

                }
            );
        }

        function createACLs(cb){
            ACL.create(
            [
                {
                    "model":"Owner",
                    "accessType": "*",
                    "principalType": "ROLE",
                    "principalId": "$everyone",
                    "permission": "DENY"
                },
                {
                    "model":"Owner",
                    "accessType": "EXECUTE",
                    "principalType": "ROLE",
                    "principalId": "$unauthenticated",
                    "permission": "ALLOW",
                    "property": "post"
                },
                {
                    "model":"Owner",
                    "accessType": "EXECUTE",
                    "principalType": "ROLE",
                    "principalId": "$unauthenticated",
                    "permission": "ALLOW",
                    "property": "login"
                },
                {
                    "model":"Owner",
                    "accessType": "READ",
                    "principalType": "ROLE",
                    "principalId": "$authenticated",
                    "permission": "ALLOW"
                },
                {
                    "model":"Owner",
                    "accessType": "WRITE",
                    "principalType": "ROLE",
                    "principalId": "$owner",
                    "permission": "ALLOW"
                }
                ,
                    {
                        "model":"Task",
                        "accessType": ACL.ALL,
                        "principalType": ACL.ROLE,
                        "principalId": Role.EVERYONE,
                        "permission": ACL.DENY
                    },
                    {
                        "model":"Task",
                        "accessType": ACL.WRITE,
                        "principalType": ACL.ROLE,
                        "principalId": Role.AUTHENTICATED,
                        "permission": ACL.ALLOW,
                        "property": "create"
                    },
                    {
                        "model":"Task",
                        "accessType": ACL.WRITE,
                        "principalType": ACL.ROLE,
                        "principalId": Role.OWNER,
                        "permission": ACL.ALLOW,
                        "property": "upsert"
                    },
                    {
                        "model":"Task",
                        "accessType": ACL.READ,
                        "principalType": ACL.ROLE,
                        "principalId": Role.AUTHENTICATED,
                        "permission": ACL.ALLOW
                    }
                /*,{
                    "model":"Owner",
                    "principalType": "ROLE",
                    "principalId": "$authenticated",
                    "permission": "ALLOW",
                    "property": "__create__Tasks"
                }, {
                    "model":"Owner",
                    "principalType": "ROLE",
                    "principalId": "$authenticated",
                    "permission": "ALLOW",
                    "property": "__count__Tasks"
                }, {
                    "model":"Owner",
                    "principalType": "ROLE",
                    "principalId": "$authenticated",
                    "permission": "ALLOW",
                    "property": "__findById__Tasks"
                }, {
                    "model":"Owner",
                    "principalType": "ROLE",
                    "principalId": "$owner",
                    "permission": "ALLOW",
                    "property": "__updateById__Tasks"
                },{
                    "model":"Owner",
                    "principalType": "ROLE",
                    "principalId": "$owner",
                    "permission": "ALLOW",
                    "property": "__destroyById__Tasks"
                },*/

            ],
            function(err, acls){
                if (err) return cb(err);
                console.log(acls);

                return cb(null, true);
            });
        }
    }
);

Below is the sample output from the DEBUG=loopback.security.* node .. I notice that if I try to access Owner related API, the loopback calls the isInRole method. But I try to access Task related API, the loopback does not call the isInRole method.

Web server listening at: http://0.0.0.0:3000
Browse your REST API at http://0.0.0.0:3000/explorer
  loopback:security:role isInRole(): $everyone +0ms
  loopback:security:access-context ---AccessContext--- +1ms
  loopback:security:access-context principals: [] +1ms
  loopback:security:access-context modelName Owner +0ms
  loopback:security:access-context modelId undefined +0ms
  loopback:security:access-context property find +0ms
  loopback:security:access-context method find +0ms
  loopback:security:access-context accessType READ +0ms
  loopback:security:access-context accessToken: +0ms
  loopback:security:access-context   id "$anonymous" +0ms
  loopback:security:access-context   ttl 1209600 +0ms
  loopback:security:access-context getUserId() null +0ms
  loopback:security:access-context isAuthenticated() false +0ms
  loopback:security:role Custom resolver found for role $everyone +0ms
  loopback:security:acl The following ACLs were searched:  +1ms
  loopback:security:acl ---ACL--- +1ms
  loopback:security:acl model Owner +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 Owner +0ms
  loopback:security:access-context  property find +0ms
  loopback:security:access-context  accessType READ +0ms
  loopback:security:access-context  permission DENY +0ms
  loopback:security:access-context  isWildcard() false +0ms
  loopback:security:access-context  isAllowed() false +0ms

  loopback:security:acl The following ACLs were searched:  +18s
  loopback:security:acl ---Resolved--- +0ms
  loopback:security:access-context ---AccessRequest--- +0ms
  loopback:security:access-context  model Task +0ms
  loopback:security:access-context  property find +0ms
  loopback:security:access-context  accessType READ +1ms
  loopback:security:access-context  permission ALLOW +0ms
  loopback:security:access-context  isWildcard() false +0ms
  loopback:security:access-context  isAllowed() true +0ms
blocked doc triaging

Most helpful comment

I know this issue was closed I while ago, but I was working on something similar and I was still having the same problem, I was creating my ACL but it did not have any effect to my API. To summarize I was just trying to deny find access to all the models using

 ACL.create({
        model: model,
        accessType: "EXECUTE",
        principalType: "ROLE",
        principalId: "$everyone",
        permission: "DENY",
        property: "find"
      }, function (err, acl) {
        console.log('ACL entry created: %j', acl);
      });

, but when I was testing from explorer to see if it worked I still was able to access all my models, the ACL had no effect.
After reading for so long I found this post , and it solve my problem.

Maybe I miss some part of the lb docs or a part of this issue's comments, but in case I help anyone, when you do ACL.create, you are just saving your ACL to your datasource, but that, apparently, does not have any impact on your app. You need to do app.models.MyModel.settings.acls.push(yourNewAcl) so that it will take effect over your app.

full example:

var acl = {
      accessType: "EXECUTE",
      principalType: "ROLE",
      principalId: "$everyone",
      permission: "DENY",
      property: "find"
    }
    app.models.MyModel.settings.acls.push(acl);

It is obvious to me now, but I leave the comment in case I help anyone.

All 7 comments

@pusaphil : Could you please explain what you're trying to achieve here by defining ACL rules in a boot-script?

I think the ACL for the model base User ('Owner' in my code) loads the default ACL for User model and the model base PersistedModel ('Task' in my code) do not have any ACL.

That's correct.

It would be great if you can provide me with your sample repo, or you can for our sandbox from here

@gunjpan, Thank you for responding.
I was trying to create the DB tables and sample data before staring the loopback application, like import initial DB and data for production setup. I put them inside server/bin instead of putting them inside of server/boot to prevent loopback from calling it again and again whenever I restart the application. I hope you get my point here. :)

The command is node server/bin/create-lb-tables.js && node server/bin/sample-data.js
After that, I run the loopback command: DEBUG=loopback.security.* node .

I already found the solution just today.
The User, AccessToken, Role, RoleMapping and ACL from DB works according to what I expected.
The following solutions work for me, and I want to share with you.

Please see below:

  1. Built-in models such as ACL, Role, RoleMapping and User should be 'extended' - meaning create your own model and based them according to what you need.
    _i.e Account model based on User model and MyACL model based on ACL Model_
  2. Creating database tables for built-in models. This is the reference: http://loopback.io/doc/en/lb2/Creating-database-tables-for-built-in-models.html. I just replace the array values of lbTables in the example according to what the model names are.
  3. Don't forget to update the dataSource at server/model-config.json to your desired dataSource - which is defined in server/datasources.json
  4. For the AccessToken, just used the built-in model and update the dataSource at server/model-config.json to your desired dataSource (no need to 'extend' this model). When I tried to 'extend' the AccessToken model, there are _no AccessToken saved in the DB table_, so just used the built-in model. Don't forget to add this model in Creating database tables for built-in models. you can 'extend' this model and apply the following relationships from this reference: https://github.com/strongloop/loopback/issues/2531#issuecomment-234399196. Don't forget to _add this model in Creating database tables for built-in models_.
  5. To all 'extended' models, add Data source-specific options in option property of your model JSON file. This is the reference: http://loopback.io/doc/en/lb2/Model-definition-JSON-file.html
    _i.e For my MyACL model, below is the sample JSON file_
    { "name": "MyACL", "base": "ACL", "idInjection": true, "options": { "validateUpsert": true, "mysql": { "table": "MyACL" } }, ... }
  6. For the ACL definition, define all properties in the table. I encounter problem here when the property field in ACL table is NULL; I noticed that the ACL definition with NULL values is not implemented. If you have a NULL value especially for property field, try to use ALL instead of NULL.

@pusaphil : I'm glad that you found the workaround and thank you for taking the time to detail the solution.
Is this good to close now?

@crandmck Can you make sure the above points are documented in the relevant sections? They are all good points to note. @pusaphil Thanks for the detailed notes. :+1:

@gunjpan Yep, this issue is good to close.
@superkhau NP. :)

I know this issue was closed I while ago, but I was working on something similar and I was still having the same problem, I was creating my ACL but it did not have any effect to my API. To summarize I was just trying to deny find access to all the models using

 ACL.create({
        model: model,
        accessType: "EXECUTE",
        principalType: "ROLE",
        principalId: "$everyone",
        permission: "DENY",
        property: "find"
      }, function (err, acl) {
        console.log('ACL entry created: %j', acl);
      });

, but when I was testing from explorer to see if it worked I still was able to access all my models, the ACL had no effect.
After reading for so long I found this post , and it solve my problem.

Maybe I miss some part of the lb docs or a part of this issue's comments, but in case I help anyone, when you do ACL.create, you are just saving your ACL to your datasource, but that, apparently, does not have any impact on your app. You need to do app.models.MyModel.settings.acls.push(yourNewAcl) so that it will take effect over your app.

full example:

var acl = {
      accessType: "EXECUTE",
      principalType: "ROLE",
      principalId: "$everyone",
      permission: "DENY",
      property: "find"
    }
    app.models.MyModel.settings.acls.push(acl);

It is obvious to me now, but I leave the comment in case I help anyone.

It does not matter if you create an acl table in the db and populate it with acls. The acls are read from the acl settings in the model, so you can push them in boot scripts or specify them in the model.
I have been trying to see its effect from the database and at last i decided to drop all acls in myacl table, it didn't have any effect on the app. What prompted all this is the question i was having, why push acls to the model settings during runtime when they are already in the database.

Was this page helpful?
0 / 5 - 0 ratings