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:
β Users:
:
β Roles:
, Piter , Bob β . Alice , , .
β Articles:
, (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'))
(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'))
(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'))
, 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
) β β
. , . , , β .
" "
[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.