I'm trying to achieve the following points with Ory Keto:
UserA has ownership of ProductA -> Ownership gives one CRUD rights.
UserB has the role Admin -> Admin gives one CRUD rights on everything.
UserA has a role KYCVerified or is part of a group named KYCVerified -> This gives the user additional permissions.
Point 1 describes the concept of ownership, which is described as one of the shortcomings of RBAC (source) with the current state of Ory Keto:
There is no concept of ownership: Dan is the author of article "Hello
World" and is thus allowed to update it.
Point 2 describes a role that basically passes the ownership check, since this role can do everything. This should be possible in the current state, but not in combination with point 1.
Point 3 describes basically the same thing as point 2, but this is more specific to my use case.
I've read this article on the Ory website and this article. However, I'm still unable to wrap my head around this concept. I've made the following example of how I see the concept of ownership with Ory Keto:
# Tenant TenantA needs to be a owner of product ProductA in order to view it
products:ProductA#view#(tenants:TenantA#owner)
# Tenant A is a owner of ProductA
tenants:ProductA#owner#TenantA
But this will result in a lot of rules and I'm not even sure if this is the way to go.
As of this moment you are right. You have to create a bunch of tuples manually. The full set of tuples should be something like:
products:ProductA#owner#UserA
products:ProductA#crud#(products:ProductA#owner)
roles:admin#member#UserB
products:ProductA#curd#(roles:admin#member)
products:ProductA#additional_permissions#(roles:KYCVerified#member)
roles:KYCVerified#member#UserA
With https://github.com/ory/keto/pull/877 you will be able to define global rewrites. It would looks similar to:
import { Context, Namespace } from #ory/keto-config
/**
* "User" is a namespace with no additional rewrite rules defined.
*/
class User implements Namespace {}
/**
* "Role"s only have members.
*/
class Role implements Namespace {
related: {
members: User[]
}
}
/**
* "Product" is a namespace representing a product. It has some rewrites.
*/
class Product implements Namespace {
// Relations are defined and type-annotated.
related: {
/**
* "owners" are the users that are the owners of the product.
*/
owners: User[]
/**
* "admins" are the roles that are administrators of this product (potentially only one).
*/
admins: Role[]
/**
* "special_roles" are the roles a user has to be member of to gain "additional_permissions"
*/
special_roles: Role[]
}
permits = {
// this is probably three/four rewrites (create, read, update, delete) with similar rules
crud: (ctx: Context): boolean =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.some((admin) => admin.related.members.includes(ctx.subject)),
// for the additional_permissions one has to have curd and be member of a special role
additional_permissions: (ctx: Context): boolean =>
this.permits.crud(ctx) &&
this.related.special_roles.some((role) => role.related.members.includes(ctx.subject))
}
}
With that you have to create these tuples:
products:ProductA#owners#UserA
roles:admin#members#UserB
roles:KYCVerified#members#UserA
products:ProductA#admins#(roles:admin)
products:ProductA#additional_permissions#(roles:KYCVerified)
Please note that it is not possible (and not planned right now) to define a single admin group that would have access to everything. You always have to have some kind of relation between the object and subject to query/rewrite it. That is the reason for having the admins and special_roles relations.
Related
I'm learning about object oriented design as I go through my first real, albeit personal, project.
I'm at the stage where I'm going to build a user object. I have 4 user subtypes:
root-admin - webmaster, site owner
group-admin - leader of a group that has a membership and page (e.g. band manager)
group-member - user that can view, post, and comment as group member on the page and see private content (e.g. band member)
unassociated user - member of the entire site that can view and comment publicly (e.g. fan)
All group admins will be group members and all group members will be unassociated users as well. So basically it's a simple user permissions hierarchy.
With that said, how do I go about setting up the architecture of my class with respect to these levels? Is there a nesting technique? I'm just struggling to hash out the scaffolding here. I have my use cases and narratives written out already.
So you're implementing role-based access control. Each user has one of four roles. So a user is an object and the users's role is one of the attributes in a user object.
enum roles {
root_admin,
group_admin,
group_member,
unassociated_user
}
class user {
string id;
roles role;
}
Next, you need to enforce the business rules that allow users of certain roles to do certain activities.
Start with a simple solution (this is always a good policy):
enum activities {
view,
post,
add_comment
}
And implement a function or class whose job is to say if an activity is allowed for a given role.
class role_based_access_control {
private:
permissions perm;
public:
bool is_permitted(activities a, roles r) {
return perm[r].contain(a);
}
}
Then, in places in your code where you implement different activities, include a call to this function:
void add_comment_to_page(user u, comment c, page p) {
if (!role_base_access_control.is_permitted(add_comment,u.role))
throw "access_forbidden";
...
}
The important thing is to keep the role based access control rules centralized so that they are visible, easy to monitor, and to audit.
If I have a document Shop that has many Activities defined as ReferenceMany, is there a way I can directly query for the list of Activities for a Shop without hydrating a Shop instance?
For example:
{
"_id": fd390j09afj09dfj,
"activities": [
...
]
}
All I want is to be able to say "get me the array activities where _id is fd390j09afj09dfj, and hydrate them as Activity instances.
Here's the first solution I came up with:
/**
* Gets all activities configured for a shop.
*
* #param string $shopId
* #return \BikeShed\Domain\Activity[]|\Doctrine\Common\Collections\ArrayCollection
*/
public function findByShopId($shopId) {
/** #var \BikeShed\Domain\Repository\Shop $shopRepository */
$shopRepository = $this->dm->getRepository('BikeShed\Domain\Shop');
$shop = $shopRepository->findOneById($shopId);
return $shop->getActivities();
}
It's simply fetching the Shop and then getting all the Activities via the defined relation.
Here's a working example of how you would implement jmikola's last suggestion:
/**
* #param string $shopId
* #return ActivityModel[]
*/
public function findByShopId($shopId) {
$partialShopData = $this->dm->getRepository('BikeShed\Domain\Shop')->createQueryBuilder()
->hydrate(false)
->field('activities')
->getQuery()
->getSingleResult()
;
$activityIds = [];
if(!empty($partialShopData['activities']))
foreach($partialShopData['activities'] as $activity)
if(!empty($activity['$id']))
$activityIds[] = $activity['$id'];
return $this->createQueryBuilder()
->field('id')
->in($activityIds)
->getQuery()
->toArray()
;
}
You cannot directly query the Shop collection or (or ODM repository) and receive Activity instances; however, you can use the Query Builder API to specify a projection with select('activities'). The executed query will still return Shop instances, but the activities field should be the only thing hydrated (as a PersistentCollection of Activity instances). In this case, you shouldn't modify any of the non-hydrated Shop fields, as ODM will detect any non-null value as a change.
It should be trivial to add a convenience method on ShopRepository that issues the above query with its select() and returns the collection (or an array) of Activity documents instead of the Shop. Keeping the Shop inaccessible should also protect you from inadvertently modifying other non-hydrated fields within it.
The down-side with this method is that the Activities will be proxy objects and lazily loaded. You can mitigate this with reference priming. With priming, you'll end up doing two queries (one for the Shop and one for all referenced Activity documents).
Regarding your follow-up question about putting this method on the Activity repository, you do have another option. Firstly, I agree that ActivityRepository::findByShopId() is preferable to calling a method on ShopRepository that returns Activity objects.
Each repository has a reference to the document manager, which you can use to access other repositories via the getRepository() method. An ActivityRepository::findByShopId() could do the following:
Access the Shop repository through the document manager
Query for the Shop by its ID, projecting only the activities field and disabling hydration completely
Collect the identifiers from the activities array. Depending on whether the Activity references are simple or not, the elements in that array may be the raw _id values or DBRef objects.
Execute a query for all Activity objects (easy, since we're already in that repository) where the ID is $in the array of identifiers
I have a site where our customers log in to see their own data. Each customer must only see their own data (of course), and different users will have access to different pages within one customer. In addition - the editors must see all data.
I want to set up the access rights based on roles to determine which customer that the user is member of, and what pages the user can access.
Groups:
Customer1Role
Customer2Role
TicketViewerRole
ChangeRequestRole
Users:
Cust1_LowLevelUser. Roles: Customer1Role, TicketViewerRole
Cust1_HighLevelUser Roles: Customer1Role, TicketViewRole, ChangeRequestRole
Cust2_LowLevelUser. Roles: Customer2Role, TicketViewerRole
Cust2_HighLevelUser Roles: Customer2Role, TicketViewRole, ChangeRequestRole
Page structure
We have created a page tree where each customer has its own "root page" with access only to their respective role. Below that node we create instances of the data specific pages, which have their access rights based on user roles as well as the customer role.
Customer1 (Customer1Role)
|--TicketsForCust1 (Customer1Role, TicketViewerRole)
|--ChangeRequestsForCust1 (Customer1Role, ChangeRequestRole)
Customer2 (Customer2Role)
|--TicketsForCust2 (Customer2Role, TicketViewerRole)
|--ChangeRequestsForCust2 (Customer2Role, ChangeRequestRole)
Burning question:
How do we prevent user Cust2_HighLevelUser from seeing ChangeRequestsForCust1?
EPiServer only checks if any role is sufficient for granting access, and since the user belongs to ChangeRequestRole, they will be granted access, regardless of the customer specific role. Is it possible to make EPiServer check BOTH the customer role, and the page role?
Or do I have to look at this from another view? Please let me know if you have run into this and solved it in another way.
Sorry, long post, but hopefully I get my point across.
There is no Deny flag in the access rights model so you need to code it yourself with that role structure.
Add code to your template base class that denies access and for the PageTree control you can do something like this:
protected void NavSubPageTreeFilter(object sender, EPiServer.Filters.FilterEventArgs e)
{
for (int i = e.Pages.Count - 1; i > -1; i--)
{
PageData pd = e.Pages[i];
if (yourUser.IsInRole("blabla") && ... etc)
{
e.Pages.RemoveAt(i);
}
}
}
I am using native CDbAuthManager to implement RBAC in my webapp. How can I get all the users who has permission to do a role? Suppose I have role named updateprofile. I want to get all the users assigned to that role. I searched the documentation and couldnt find a function.
(I know i can iterate through all user models and do a checkAccess() in a foreach loop, but I prefer a nicer solution )
The easiest way I've found to do this is to create an AuthAssignment model that maps to your auth_assignment table. Then, you can setup relationships, scopes, etc for it and query using it to retrieve all user models. There isn't anything particularly special about the auth_assignment table (as it is mainly just roles in there).
code like
class AuthAssginment extends CActiveRecord{.....
public function getUsersBaseOnRole($role) {
return Yii::app()->db->createCommand()
->select('userid')
->from($this->tableName())
->where('itemname=:role', array(
':role' => $role,))
->queryAll() ;
}....
I think the other replies do not give you the perfect result because roles can be hierarchical and so, you cannot use direct queries or relations. My solution which works well is:
// list all users with 'userManagement' role
if($users = Users::model()->findAll()) {
foreach($users as $id => $user) {
if(!$user->checkAccess('userManagement')) {
unset($users[$id]);
}
}
$users = array_values($users); // to reset indices (optional)
}
I'm using Doctine2\ORM and i have en entity for user , and the role.
When user is registering, i need to create role record with user id and return its id to user, then create the user record, how can i organize my annotation for such work?
You can add a default role to your user during __construct, then mark the association as cascade={"persist"}.
The constructor would look like:
public function __construct()
{
$this->role = new RoleLink();
}
I also don't think that a role needs to keep a reference to the user itself, but if need that, keep in mind that in Doctrine 2 ORM you handle associations by assigning related objects to the association property itself (not identifiers!)
Firstly, I don't think you want to create your role entry when a user is registering. I think it's wise to define these upfront (along with their respective access rights).
Once you've taken care of that I guess the obvious relation you're after would be ManyToOne on the User with a persistence cascade set.
/**
* #Table(name="role")
*/
class Role
{
// possibly define your roles accesses as another realtion or hard code?
}
/**
* #Table(name="user")
*/
class User
{
// id + any other definitions
/**
* #var Entities\Role $role
* #ManyToOne(targetEntity="Role", cascade={"persist"})
*/
private $role;
}