I have a CDS view and would like to apply authorization checks.
CDS View ZCDS_VIEW
#AbapCatalog.sqlViewName: 'ZCDS_VIEW'
#VDM.viewType: #BASIC
#AccessControl.authorizationCheck: #CHECK
define view ZCDS_VIEW
as select distinct from vbak
inner join vbap on vbap.vbeln = vbak.vbeln // At least 1 item
[...]
{
key vbak.vbeln,
vbak.ktext,
[...]
}
where
[...].
My concern is that the way to control authorizations requires checks in different tables and not just an authorization check on a field in the CDS view.
Indeed, I must:
Check the authorizations on the profit center of a specific table ZT1
Make a join of the entries of the ZT1 table with a ZT2 table which gives me the authorized divisions
Filter the results of my CDS view with the authorized divisions.
To do that I did like this:
Create CDS ZCDS_AUTH_PLANT
#AbapCatalog.sqlViewName: 'ZCDS_AUTH_PLANT'
#VDM.viewType: #BASIC
#AccessControl.authorizationCheck: #CHECK
define view ZCDS_AUTH_PLANT
as select distinct from zt1
inner join zt2 on zt2.bu = zt1.bu
{
zt1.prctr as profit_center,
zt2.bukrs as company_code,
zT2.werks as plant_code
};
Create DCL ZDCL_AUTH_PLANT
#MappingRole: true
define role ZDCL_AUTH_PLANT {
grant
select
on
ZCDS_AUTH_PLANT
where
( profit_center ) = aspect pfcg_auth( XXX, PRCTR );
}
Update CDS ZCDS_VIEW
Addition of the join condition on ZCDSC4_AUTH_PLANT to have authorized divisions.
#AbapCatalog.sqlViewName: 'ZCDS_VIEW'
#VDM.viewType: #BASIC
#AccessControl.authorizationCheck: #CHECK
define view ZCDS_VIEW
as select distinct from vbak
inner join ZCDSC4_AUTH_PLANT on ZCDSC4_AUTH_PLANT.plant_code = vbap.werks // At least 1 item matching division
[...]
{
key vbak.vbeln,
vbak.ktext,
[...]
}
where
[...].
I wanted to know:
Is this a good practice?
Do you see a more relevant alternative?
Should we do everything in the DCL?
Is this a good practice?
The DCL is the standard way to implement authorization checks for ABAP CDS views, so yes, using it is a good practice.
This is included in the official documentation here Access Control for CDS Entities and here Creating DCL Sources
Do you see a more relevant alternative?
Is using a DCL file not working for your requirements? If it is working, you are already using the correct way to implement auth checks, so no alternative solution is needed
Should we do everything in the DCL?
If everything means the complete authorization checks, then yes.
You can also have more complex logic in the DCL of course than only checking for the PCFG object (for instance for GDPR limitations), but it doesn't seem to apply to your scenario
Related
I have a table that describes a document with title, content, created_at, etc and a struct that describes columns to filter that table
struct Partial {
handle: String,
title: String,
content: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
which; after some transformations, I pass to the database layer and depending on who the user is, return a set of rows.
For example, if user is the author, user can search any columns, otherwise they can search only publicly visible documents.
A user can also 'share' a document with another user so if there a share from the requester to the document owner, the user should also be able to search in those documents as well.
There are two ways to go about that (as far as I know)
Use inner joins and filter on columns
INNER JOIN author on document.id = author.document_id
INNER JOIN visibility on document.id = visibility.document_id
INNER JOIN share on document.id = share.document_id
...
...
...
WHERE document.handle = $handle -- from `Partial` struct
WHERE document.title = $title -- from `Partial` struct
WHERE document.content = $content -- from `Partial` struct
WHERE author.user_id = $user_id -- from user info passed to db
WHERE visibility.scope = 'PUBLIC' -- from user info passed to db
WHERE share.shared_to = $user_id -- from user info passed to db
WHERE group.member = $user_id -- from user info passed to db
...
...
...
This requires passing user information ($user_id for this example but this also sometimes includes additional info like if the user is blocked or the account is non-public, belongs to a group, etc. Around 12 properties in total) to the database layer.
generate a list of ids in application layer and pass it on to the database layer.
For example,
let public_document_ids = visibility.find(Find { visibility: Visibility::Public }).map(|visibility_row| visibility_row.document_id).collect();
let authored_document_ids = author.find(Find { document_author: user_id }).map(|authored_row| authored_row.document_id).collect();
let shared_document_ids = share.find(Find { shared_to: user_id }).map(|share_row| share_row.document_id).collect();
and then pass sum of all of these ids to database
WHERE document.handle = $handle -- from `Partial` struct
WHERE document.title = $title -- from `Partial` struct
WHERE document.content = $content -- from `Partial` struct
WHERE document.id IN $ids -- from `ids` passed to db
...
...
...
To me it seems that the first approach of using inner joins and filtering on columns in the database can
potentially be more efficient,
and a lot more easier to get right
but the second approach, generating a list of ids in the application layer, allows
a lot more flexibility and
also lets me use different (distributed) databases to store different tables (perhaps even different database drivers)
It also reduces the amount of user information passed to the database layer
but it also requires loading all the data into the application layer before filtering the results
and implementing pagination will be lot more complex
So my question - is there a way to know which approach I should go with? Perhaps there's something I haven't considered yet.
I have a ACF relationship field for a custom post type, properties. Moreover, the content of these properties can either be in English or Spanish. When I use a ACF relationship field to associate properties to a user on the user edit page -- /wp/wp-admin/user-edit.php -- everything works as expected, and I can select from the relationship drop down the properties I want for this user.
My question is this: how can I write a query inside this filter such that only the English properties appear, regardless of the language of the page (English or Spanish) which is set by the WPML toggle? I know how to write such a query in SQL:
SELECT *
FROM wp_2_posts
INNER JOIN wp_2_icl_translations
ON wp_2_icl_translations.element_id = wp_2_posts.id
AND wp_2_icl_translations.language_code = 'en'
WHERE wp_2_posts.post_type = 'properties';
But the filter requires that changes be made to $args which adhere to WP_Query. I do not know how to write the above INNER JOIN on wp_2_icl_translations to only show custom post types properties in English. Can someone please instruct me how so that I can get the acf filter to work the way I need it to?
Ok so the way I was able to just show English properties on the user admin page was with this posts_request hook:
add_filter('posts_request', function($sql, $query) {
$is_user_edit_page = (
isset($_SERVER['HTTP_REFERER']) &&
strpos($_SERVER['HTTP_REFERER'], 'user-edit') !== false
);
$is_property_sql = (strpos($sql, 'property') !== false);
if ($is_user_edit_page && $is_property_sql) {
$sql = str_replace("'sp'", "'en'", $sql);
}
return $sql;
}, 10, 2);
In this hook I make sure it only runs on the user-edit page and that the sql it's changing relates to the properties. If all those cases are true, then I just replace the Spanish language code with the English one. And as a result, the SQL used to query the properties is forced to only query for English ones.
I have written one extension for making service order.
The issue I am facing here is,
There are FE users belong to three FE user groups namely "client", "Admin" and "Employee".
Here the client can make order and he should be able to see only his orders.
And the admin can see all orders done by different clients.
And the employee should be able to see only some clients orders.
Currently I made a order table with N:1 relation with FE user table. So every order should relate with any one client.
So in controller, I am checking the login user and using custom query in repository, I am accessing order related to loggedin client (FE user)
In file OrdersController.php
public function listAction() {
$orders = $this->ordersRepository->orderForLoginUsr();
$this->view->assign('orders', $orders);
}
In file OrdersRepository.php
public function orderForLoginUsr(){
$loggedInUserId = $GLOBALS ['TSFE']->fe_user->user['uid'];
$query = $this->createQuery();
$query->matching(
$query->equals('user', $loggedInUserId)
);
$query->setOrderings(array('crdate' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING));
return $query->execute();
}
But here my question is how to make admin user able to see all the orders done by all clients?
I have to write different template and action that calling function findAll() ?
$orders = $this->ordersRepository->findAll();
And how to set for usergroup Employee ?
Thanks in Advance
I think that the easiest way is to actually implement 3 actions with 3 different plugins, something like: listClientAction, listAdminAction and listEmployeeAction
In each of those action, you implement a method in your repository that fetch the right list of order with the good ordering:
orderForLoginClient(), orderForLoginEmployee(), orderForLoginAdmin()
What does the trick actually is that there will be 3 plugins on your page, one for each action. In each instance of your plugin, you set the access for the right be_group.
Don't forget to add the actions and plugin in the localconf and ext_table files.
I hope it will help!
Olivier
If your view is almost the same for client, admin, employee you should simply add a method like getOrderWithPermissionsForUser($currentUser);
In the method itself you should check for the usergroup and call different queries on your Repo.
If your view is different from usergroup to usergroup, you should use different templates with partials for the same parts.
If the data of the views is the same, just change the template for each usergroup in the action. If not use different actions.
Here is a helper method for easily changing your templatefile.
/**
* This method can change the used template file in an action method.
*
* #param string $templateName Something like "List" or "Foldername/Actionname".
* #param string $templateExtension Default is "html", but for other output types this may be changed as well.
* #param string $controllerName Optionally uses another subfolder of the Templates/ directory
* By default, the current controller name is used. Example value: "JobOffer"
* #param \TYPO3\CMS\Fluid\View\AbstractTemplateView $viewObject The view to set this template to. Default is $this->view
*/
protected function changeTemplateFile($templateName, $templateExtension = 'html', $controllerName = null, AbstractTemplateView $viewObject = null)
{
if (is_null($viewObject)) {
$viewObject = $this->view;
}
if (is_null($controllerName)) {
$controllerName = $this->getControllerContext()->getRequest()->getControllerName();
}
$templatePathAndFilename = $this->getTemplateRootpathForView($controllerName . '/' . $templateName . '.' . $templateExtension);
$viewObject->setTemplatePathAndFilename($templatePathAndFilename);
}
I have a question about the rbac system. I think I've pretty well understood it but I need more informations about a special case.
I would like to do the autorisations on groups instead of users. I mean for instance the group "HR" has permission to create a person. Then any person who join this group would have it as well.
Let me give you more informations.
A part of my database:
And this a part of what my group hierarchy could be:
So what I'm looking for, this would be a must, is a system where each group has some autorizations. People get the autorizations of their group and of their parents group (for instance people in "Forsys" has the autorizations of "Forsys", "R&D" and "Administration").
The solution I see at the moment is using bizrule. But I'm not sure write php code in database is a good idea and then if I update the group hierarchy (R&D inherits of RH instead of Administration) I would have to modify bizrule in database. I tried it and it works well but as you can see it require a lot of code.
$user = User::model()->with("people","people.groups")->findByPk(Yii::app()->user->id);
foreach($user->people[0]->groups as $group)
if($group->id == 2)
return true;
return false;
It's just for see if a user is in a group (without checking parent groups and hierarchy)
Another possibility could be create a new table "group_auth" where we would say for instance:
-Group_2 has role "managePerson"
-Group_3 has operation "deleteUser"
...
And then everytime a user is added in or removed of a group we would update his autorizations in the auth_assigment table.
I'd like to hear other opinions on this subject.
All comments will be appreciated :)
Thank you for reading and sorry for my English if you had difficulties to understand me.
Michaƫl S.
Do users ever get their own authorization items? If not, seems like you could in essence swap out the userid column in auth_assignment and name it / treat it as groupID instead. That way you wouldn't need to worry about keeping user auth assignments in sync with your group roles.
A couple of places you'd probably need to make some changes:
- by default CWebUser passes in the logged in userid for use in bizrules. Might be good to change that our with your own override that passes in groupId/groupIds instead.
- you'd need to override CDbAuthManager and rework some of how things work there
We've done something similar on a project I've worked on (we were handling multi-tenant RBAC custom permissions), which required custom CDbAuthManager overrides. It gets a bit tricky if you do it, but there is an awful lot of power available to you.
Edit:
Understood about your users sometimes needing to have additional authorizations. What if your group has a 'roles' field with different roles serialized in it (or some other method of having multiple roles stored for that group, could also be a relationship).
Then, on user login (for efficiency), you'd store those roles in session. Probably the easiest way to handle things would be to write a custom checkAccess for your WebUser override:
https://github.com/yiisoft/yii/blob/1.1.13/framework/web/auth/CWebUser.php#L801
as that will make things simpler to do your custom checking. Then I'd probably do something like:
if(Yii::app()->user->hasGroupAccess() || Yii::app()->user->checkAccess('operation/task/role')) {
....
}
In your WebUser hasGroupAccess method, you could loop over all group roles and send those to checkAccess as well.
Think that will work?
What I use to check access for groups when it's in another table, or somewhere else in the application I give the user the role per default. By using this:
return array(
'components'=>array(
'authManager'=>array(
'class'=>'CDbAuthManager',
'defaultRoles'=>array('authenticated', 'R&D', 'Administration'),
),
),
);
Under: Using Default Roles
By using this, every user gets these assignments. Now, I create a business rule to make sure that the checkAccess('group') will return the correct value.
For example in your case the business rule for R&D would be:
return (
count(
Person::model()->findByPk(Yii::app()->user->id)->groups(array('name'=>'R&D'))
) > 0
) ? true : false;
So what this does is:
find the logged-in person by primary key
look into groups (from the user) for the group with name R&D
if there is a group: return true (else return false)
I have a model Page, which can have Posts on it. What I want to do is get every Page, plus the most recent Post on that page. If the Page has no Posts, I still want the page. (Sound familiar? This is a LEFT JOIN in SQL).
Here is what I currently have:
Page.objects.annotate(most_recent_post=Max('post__post_time'))
This only gets Pages, but it doesn't get Posts. How can I get the Posts as well?
Models:
class Page(models.Model):
name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add = True)
enabled = models.BooleanField(default = True)
class Post(models.Model):
user = models.ForeignKey(User)
page = models.ForeignKey(Page)
post_time = models.DateTimeField(auto_now_add = True)
Depending on the relationship between the two, you should be able to follow the relationships quite easily, and increase performance by using select_related
Taking this:
class Page(models.Model):
...
class Post(models.Model):
page = ForeignKey(Page, ...)
You can follow the forward relationship (i.e. get all the posts and their associated pages) efficiently using select_related:
Post.objects.select_related('page').all()
This will result in only one (larger) query where all the page objects are prefetched.
In the reverse situation (like you have) where you want to get all pages and their associated posts, select_related won't work. See this,this and this question for more information about what you can do.
Probably your best bet is to use the techniques described in the django docs here: Following Links Backward.
After you do:
pages = Page.objects.annotate(most_recent_post=Max('post__post_time'))
posts = [page.post_set.filter(post_time=page.most_recent_post) for page in pages]
And then posts[0] should have the most recent post for pages[0] etc. I don't know if this is the most efficient solution, but this was the solution mentioned in another post about the lack of left joins in django.
You can create a database view that will contain all Page columns alongside with with necessary latest Post columns:
CREATE VIEW `testapp_pagewithrecentpost` AS
SELECT testapp_page.*, testapp_post.* -- I suggest as few post columns as possible here
FROM `testapp_page` LEFT JOIN `testapp_page`
ON test_page.id = test_post.page_id
AND test_post.post_time =
( SELECT MAX(test_post.post_time)
FROM test_post WHERE test_page.id = test_post.page_id );
Then you need to create a model with flag managed = False (so that manage.py sync won't break). You can also use inheritance from abstract Model to avoid column duplication:
class PageWithRecentPost(models.Model): # Or extend abstract BasePost ?
# Page columns goes here
# Post columns goes here
# We use LEFT JOIN, so all columns from the
# 'post' model will need blank=True, null=True
class Meta:
managed = False # Django will not handle creation/reset automatically
By doing that you can do what you initially wanted, so fetch from both tables in just one query:
pages_with_recent_post = PageWithRecentPost.objects.filter(...)
for page in pages_with_recent_post:
print page.name # Page column
print page.post_time # Post column
However this approach is not drawback free:
It's very DB engine-specific
You'll need to add VIEW creation SQL to your project
If your models are complex it's very likely that you'll need to resolve table column name clashes.
Model based on a database view will very likely be read-only (INSERT/UPDATE will fail).
It adds complexity to your project. Allowing for multiple queries is a definitely simpler solution.
Changes in Page/Post will require re-creating the view.