Flexible authorization using Casbin and PERM. Practical example

After writing the previous article on the PERM language and the Casbin library , questions arose . And not just one person, and I wanted to answer first in a comment, but I realized that the amount of material is beyond the scope of a usual comment, so I will present this answer as a separate article.







For a long time I could not understand the specific construction that sat in the head of the questioners, but in the end, after clarifying questions, I received answers, the compilation of which I will give in the quote.







And how with such DSLs the problem is solved β€œto show the list of objects that I can see? It is necessary to somehow translate it into an SQL query, not to rake out all the records from the database.



There is an interface on the site showing a list of something. Let's say - articles in the CMS admin area. There are tens of thousands of articles from the database, but usually the user has access to only a dozen. How to get articles from the database that are visible to a specific user? Well, if we have all the rules that anyone can see - taken out of the code into some kind of DSL?

In other words - how to write a query like



select * from articles a

join roles r on r.userId = currentUserId

where article.owner = currentUserId

OR (r.role in ['admin', 'supevisor']) - total admin

OR (r.domain = currentDomainId AND r.role in ['domain-admin', 'domain-supervisor']) - domain admin



I have such rules in the code, in the form of LINQ-expressions, and I can solve this problem. And such a task arises even more often than "check if there is access to one object unloaded from memory"

I hope I understood this construction correctly and during reverse reverse engineering, I was able to pull out the initial data for solving this problem. To begin with, we will do without using multi-tenancy (domains), since they complicate the task and, accordingly, understanding. I gave an example of their use in the last article.







, , , Casbin.









CMS, . user



.       , admin



supervisor



. supervisor



, admin



supervisor



, , .







, :



CMS:

DB table schema







β€” Users:

Contents of the Users table







:

β€” Roles:

Roles table content







, Piter , Bob β€” . Alice , , .







β€” Articles:

Contents of the Articles table







, (Piter, id=3) :







select * from articles a
left join roles r on r.userId = 3
where a.owner = 3
OR (r.role in ('admin', 'supevisor'))
      
      





Sample result for administrator







(Bob, id=2) :







select * from articles a
left join roles r on r.userId = 2
where a.owner = 2
OR (r.role in ('admin', 'supevisor'))
      
      





Sample result for supervisor







(Alice, id=1) :







select * from articles a
left join roles r on r.userId = 1
where a.owner = 1
OR (r.role in ('admin', 'supevisor'))
      
      





Sample result for user







, Casbin.







Casbin



PERM β€” , .

.. , () . ( Id=1 ).







, , β€” RBAC.

RBAC , . RBAC user



, author



user



(.. ), , admin



.

, , . user



supervisor



admin



, β€” , . , user



, . admin



supervisor



, .

RBAC, , -, , .

: RBAC vs. ABAC







, (user



, supervisor



,admin



) β€” β€”



. , . , , β€” .







" "



RBAC (rbac_model.conf



), :







[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
      
      





, Roles. . *.csv



, . cvs rbac_policy.csv



:







p, user, article, read
p, user, article, modify
p, user, article, create
p, user, article, delete

g, supervisor, user
g, admin, supervisor

g, 1, user
g, 2, supervisor
g, 3, admin
      
      





, user



, , . supervisor



user.



admin



supervisor



.

alice(1) user



, bob(2) supervisor



, piter(3) β€” admin



.

, , .







, . cross-cutting concern CQRS+MediatR







public IList<Article> GetArticlesForAdminPanel(int currentUserId)
{
    var e = new Enforcer("CasbinConfig/rbac_model.conf", "CasbinConfig/rbac_policy.csv");

    var obj = "article";
    var act = "read";

    // ,       
    if (e.Enforce(currentUserId.ToString(), obj, act))
    {
        //   
        var currentUserRoles = e.GetRolesForUser(currentUserId.ToString());
        //,      
        var isAdmin = currentUserRoles.Any(x => x == "admin" || x == "supervisor");

        // ,   ,   ,   
        if (!isAdmin) return _context.Articles.Where(x => x.OwnerId == currentUserId).ToList();
        else return _context.Articles.ToList();
    }
    else
    {
        //  ,  
        throw new Exception("403.       ");   
    }
}
      
      





! , .







" "



. , , - . , , user



, supervisor



admin



.







, rbac_with_abac_model.conf



:







[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = (r.sub == r.obj.OwnerId.ToString() || g(r.sub, "supervisor")) && g(r.sub, p.sub) && r.act == p.act
      
      





, [matchers]



, r.obj == p.obj



(r.sub == r.obj.OwnerId.ToString() || g(r.sub, "supervisor"))



. r.sub (id ) r.obj.OwnerId (id ) r.sub "supervisor". admin



supervisor



admin



.







, . :







public void UpdateArticle(int currentUserId, Article newArticle)
{
    var e = new Enforcer("CasbinConfig/rbac_with_abac_model.conf", "CasbinConfig/rbac_policy.csv");

    var act = "modify";

    //,       
    if (e.Enforce(currentUserId.ToString(), newArticle, act))
    {
        //,   
        _context.Articles.Update(newArticle);
        _context.SaveChanges();
    }
    else
    {
        //  ,  
        throw new Exception("403.  ");
    }
}
      
      





, e.Enforce



, Article



.







β€” .









- , user



, supervisor



β€” , admin



.

- PERM, delete_model.conf



:







[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = (r.sub == r.obj.OwnerId.ToString() || g(r.sub, "admin")) && g(r.sub, p.sub) && r.act == p.act
      
      





, , admin



. supervisor



, .







, :







public void DeleteArticle(int currentUserId, Article deleteArticle)
{
    var e = new Enforcer("CasbinConfig/delete_model.conf", "CasbinConfig/rbac_policy.csv");

    var act = "delete";

    //,       
    if (e.Enforce(currentUserId.ToString(), deleteArticle, act))
    {
        // 
        _context.Articles.Remove(deleteArticle);
        _context.SaveChanges();
    }
    else
    {
        //  ,  
        throw new Exception("403.  ");
    }
}
      
      







, , Casbin PERM .







, , . , , .







Casbin DynamicExpresso.Core C# , Casbin .







, Casbin , , API. UI .







I posted a fully functional and self-sufficient example code that I used to write this article on my Github , you can download and play if you are interested and willing.







useful links






All Articles