I'm now working with rbac yii2.
I defined all roles and permissions in database.
Now the question for me is where should I check access rule?
For example author just can update his own posts.
I saved rule in database too as rule_name = 'isAuthor'.
But I am confused about the check condition to access own posts.
Here is my actionRule:
<?php
public function actionRule(){
$auth = Yii::$app->authManager;
$rule = new \app\rbac\AuthorRule;
$auth->add($rule);
$updateMobile = $auth->createPermission('mobile/update');
// add the "updateOwnMobile" permission and associate the rule with it.
$updateOwnMobile = $auth->createPermission('updateOwnMobile');
$author = $auth->createPermission('author');
$updateOwnMobile->description = 'Update own mobile';
$updateOwnMobile->ruleName = $rule->name;
$auth->add($updateOwnMobile);
// "updateOwnMobile" will be used from "updatePost"
$auth->addChild($updateOwnMobile, $updateMobile);
// allow "author" to update their own posts
$auth->addChild($author, $updateOwnMobile);
}
?>
Where and how can I implement that?
in Controller? behavior?
or other places?
In the case of the upated for author own post the check (can('author') method ) should be placed in controller for two reason at least:
You should check not only if the use has role/permission author but also if the owner of the post is the user
Once check if the user can or not perform the related code you must drive the correct response (render the update form if the user can or a access denied message if not)
These are tipically controller/action operations ..
These are just some first suggestions
Ideally you should have a class (extending from Yii\rbac\Rule) associated with the rule. Then implement the execute function that checks whether the user is the author of the post and he has access to update the post.
Something like this:
class SiteRule extends Rule
{
//you can modify this to suit your needs.
public function execute($user, $item, $params)
{
//get user ie: \dektrium\user\models\User::findIdentity($user)
//check if the $user is the author - using defined author or created-by attribute in $params.
//return true/false
}
}
then in your controller/action you can use the CheckAccess() method (yii\rbac\ManagerInterface) to check if user has access:
if(\yii::$app->user->can('author', ['post'=>\Yii::$app->request->post()(or your model]))
{//logic here}
Related
I have installed yii2mod/yii2-rbac from this url - https://github.com/yii2mod/yii2-rbac in yii2-basic.
everything is working fine except using/allowing owner data.
from this link:https://www.yiiframework.com/doc/guide/2.0/en/security-authorization
I have created a folder in root rbac and file AuthorRule.php and code:
namespace app\rbac;
use yii\rbac\Rule;
//use app\models\Post;
/**
* Checks if authorID matches user passed via params
*/
class AuthorRule extends Rule
{
/**
* #var string
*/
public $name = 'isAuthor';
/**
* #param string|int $user the user ID.
* #param Item $item the role or permission that this rule is associated with
* #param array $params parameters passed to ManagerInterface::checkAccess().
* #return bool a value indicating whether the rule permits the role or permission it is associated with.
*/
public function execute($user, $item, $params)
{
return isset($params['post']) ? $params['post']->createdBy == $user : false;
}
}
but when I try to add the rule in permission(either AuthorRule or isAuthor under permission I created updateOwnRecord, I am getting the error, the rule doesn't exist.
What I am missing here?
but when I try to add the rule in permission(either AuthorRule or
isAuthor under permission I created updateOwnRecord, I am getting the
error, the rule doesn't exist
Not sure where you are getting the error you mentioned as there is no relevant code, but looking at your details i recon you havent understood the process correctly.
Create a permission updatePost in the auth_item .
Add AuthorRule class's serialized instance to auth_rule table.
Create a new permission updateOwnPostand specify the rule name i.e isAuthor.
Add the permission updatePost as a child to UpdateOwnPost in the auth_item_child table.
the isAuthor will be the name of the rule that you will supply to the updateOwnPost permission's rule_name column.
Add the updatePost as a child of the role you want to use the rule for, like user or anyother you have created for the standard user role.
See the below code you can run it once via any temporary action for now, we will discuss it's place later in the answer below.
$auth = Yii::$app->authManager;
$updatePost = $auth->getPermission('updatePost');
//change it to whichever role you want to assign it like `user` `admin` or any other role
$role = $auth->getRole('user');
// add the rule
$rule = new \app\rbac\AuthorRule;
$auth->add($rule);
// add the "updateOwnPost" permission and associate the rule with it.
$updateOwnPost = $auth->createPermission('updateOwnPost');
$updateOwnPost->description = 'Update own post';
$updateOwnPost->ruleName = $rule->name;
$auth->add($updateOwnPost);
// "updateOwnPost" will be used from "updatePost"
$auth->addChild($updateOwnPost, $updatePost);
// allow "author" to update their own posts
$auth->addChild($role, $updateOwnPost);
Now if all goes well and you can add a rule by running the code above
Remember You need to check the updatePost rule in the check Yii::$app->user->can() and not updateOwnPost and pass the Post model instance along as the second parameter
Like this
if (\Yii::$app->user->can('updatePost', ['post' => $post])) {
// update post
}
About The code Placement in the current application
If you want to have a separate interface where you can add create all with a form then you can follow dektrium-rbac code available already where it provides complete crud that you can use according to your own requirements.
For the reference see below
Add Rule Form
RuleController::actionCreate
RuleModel::create()
Note: if you have a lot of controllers and you want to associate this rule with every update action inside the controllers (Given that all the associated models have the created_by field) then you might go for the console\Controller and run such processes via console, so that every new controller/update can be associated with the rule repeating the above process inside a loop. For the console controller usage in basic-app see here
I am able to control my application using ACL, everything done perfectly and application is working smooth with ACL and Auth.
Now the problem is:
I have two tables, users and posts. there is no RBAC (role based access control).
I am setting deny and allow for each user like follow.
//allow User1 to do everything
$user->id=1;
$this->ACL->allow($user,'controllers');
//allow User2 to add, edit and view the posts
$user->id=2;
$this->Acl->deny($user, 'controllers');
$this->Acl->allow($user, 'controllers/Posts');
but here I am getting one problem:
user2 is getting access to edit the posts of user1.
example:
User1 created a post1.
now User2 logged in now he can edit the User1's post (i.e. post1- /localhost/myApp/posts/edit/1)
Question: How can I set ACL permission to this problem, The owner of the post can only edit the post and others can not.
I can achieve this in controller level simply checking
if($_SESSION['Auth']['User']['id'] == $Post['Post']['user_id']){
// you're the owner, so u can edit
}else{
//u cant edit, this is not ur post
}
but I need ACL to work here, Is it possible?, Please help
Thanks
here's how I would do
first of all tell Cake that Post model is an ACO
// Post.php model file
$actsAs = array('Acl' => array('type' => 'controlled'));
this way every time you create a new post cake will automatically create an item in the acos table.
pay attention: you'll have to manually create the node for the previously created Posts, this way:
// for every Post in your posts table
$this->Acl->Aco->create(array('alias' => 'Post', 'id' => 123));
$this->Acl->Aco->save();
then you have to define a parentNode() function in your Post Model file
// Post.php model file
public function parentNode() {
return null;
}
Now the ACL auth handler check form permission just at an action level. In other words it just checks that you're allowed to access the action. Then it demands other checks at a controller level by the isAuthorized() function.
so first you have to set the permission for every node
$this->Acl->allow($user, 'controllers/Posts/edit/123')
then in your controller you have to do
// PostsController.php
public function isAuthorized($user = null) {
if ($this->request->action === 'edit') {
$user = // retrieve the user array. i.e. from Session
$post_id = $this->request->$this->request->pass[0];
$post = array('alias' => 'Post', 'id' => $post_id );
return this->Acl->check($user, $post);
}
return parent::isAuthorized($user);
}
you can also implement parentNode() function to return the owner of the Post instead of null
// Post.php model file
// just an hint, the actual code should be
// a bit more complex
public function parentNode() {
$user_id = $this->field('user_id');
return array('User' => array('id' => $user_id));
}
this way don't have to set the permission for every single post because cake will check if the user has access to the parent node of the Post (who is a user too). So you just have to set the permission for every user
$this->Acl->allow($user, $user);
If you follow this method remember to set the user as an ACO too
// User.php Model file
$actsAs = array('Acl' => array('type' => 'both'));
I did not test the code above so I guess there are a lot of typos and errors too. If I have time i'll do some tests and improve my answer in the next days
I'am using CakePhp3 for my website and I have to inject some custom validation logic based on the current user Id when I'am creating or modifying an entity.
The basic case is "Is the user allow to change this field to this new value" ? If' not, I want to raise a validation error (or an unauthorized exception).
In cakephp, for what I'am understanding, most of the application and businness rules must be placed on Models or 'ModelsTable'of the ORM. But, in this classes, the AuthComponent or the current session is not available.
I don't want to call manually a method on the entity from the controller each time I need to check. I would like to use a validator, something like :
$validator->add('protected_data', 'valid', [
'rule' => 'canChangeProtectedData',
'message' => __('You're not able to change this data !'),
'provider' => 'table',
]);
Method on ModelTable :
public function canChangeProtectedData($value, array $context)
{
\Cake\Log\Log::debug("canChangeProtectedData");
// Find logged user, look at the new value, check if he is authorized to do that, return true/false
return false;
}
I cakephp < 3, the AuthComponent have a static method 'AuthComponent::user()' that is not available anymore. So, how Can I do that in CakePhp 3 ?
Thank you for any response.
EDIT - Adding more details
So here are more details. In case of an REST API. I have an edit function of an entity. The "Article" Entity.
This Article has an owner with a foreign key on the column named "user_id" (nothing special here). My users are organized in groups with a leader on the group. Leaders of groups can change article's owner but "basics" users can't do it (but they can edit their own articles). Admin users can edit everything.
So the edit method must be available for any authenticated user, but changing the "user_id" of the entity must be allowed and checked depending the case (if I'am admin yes, if I'am leader yes only if the new Id is one of my group and if I'am basic user no).
I can do this check on the controller but if I want this rule to be checked everywhere in my code where an Article is modified (in another method than the "Edit" of ArticlesController). So for me the Model seems the good place to put it no?
Authentication vs Authorisation
Authentication means identifying an user by credentials, which most of the time boils down to "Is a user logged in".
Authorisation means to check if an user is allowed to do a specific action
So don't mix these two.
You don't want validation you want application rules
Taken from the book:
Validation vs. Application Rules
The CakePHP ORM is unique in that it uses a two-layered approach to
validation.
The first layer is validation. Validation rules are intended to
operate in a stateless way. They are best leveraged to ensure that the
shape, data types and format of data is correct.
The second layer is application rules. Application rules are best
leveraged to check stateful properties of your entities. For example,
validation rules could ensure that an email address is valid, while an
application rule could ensure that the email address is unique.
What you want to implement is complex application logic and more than just a simple validation, so the best way to implement this is as an application rule.
I'm taking a code snippet from one of my articles that explains a similar case. I had to check for a limitation of languages (translations) that can be associated to a model. You can read the whole article here http://florian-kraemer.net/2016/08/complex-application-rules-in-cakephp3/
<?php
namespace App\Model\Rule;
use Cake\Datasource\EntityInterface;
use Cake\ORM\TableRegistry;
use RuntimeException;
class ProfileLanguageLimitRule {
/**
* Performs the check
*
* #link http://php.net/manual/en/language.oop5.magic.php
* #param \Cake\Datasource\EntityInterface $entity Entity.
* #param array $options Options.
* #return bool
*/
public function __invoke(EntityInterface $entity, array $options) {
if (!isset($entity->profile_constraint->amount_of_languages)) {
if (!isset($entity->profile_constraint_id)) {
throw new RuntimeException('Profile Constraint ID is missing!');
}
$languageLimit = $this->_getConstraintFromDB($entity);
} else {
$languageLimit = $entity->profile_constraint->amount_of_languages;
}
// Unlimited languages are represented by -1
if ($languageLimit === -1) {
return true;
}
// -1 Here because the language_id of the profiles table already counts as one language
// So it's always -1 of the constraint value
$count = count($entity->languages);
return $count <= ($languageLimit - 1);
}
/**
* Gets the limitation from the ProfileConstraints Table object.
*
* #param \Cake\Datasource\EntityInterface $entity Entity.
* #return int
*/
protected function _getConstraintFromDB(EntityInterface $entity) {
$constraintsTable = TableRegistry::get('ProfileConstraints');
$constraint = $constraintsTable->find()
->where([
'id' => $entity['profile_constraint_id']
])
->select([
'amount_of_languages'
])
->firstOrFail();
return $constraint->amount_of_languages;
}
}
I think it is pretty self-explaining. Make sure your entities user_id field is not accessible for the "public". Before saving the data, just after the patching add it:
$entity->set('user_id', $this->Auth->user('id'));
If you alter the above snippet and change the profile_constraint_id to user_id or whatever else you have there this should do the job for you.
What you really want is row / field level based authorisation
Guess you can use ACL for that, but I've never ever had the need for field based ACL yet. So I can't give you much input on that, but it was (Cake2) and still is (Cake3) possible. For Cake3 the ACL stuff was moved to a plugin. Technically it is possible to check against anything, DB fields, rows, anything.
You could write a behavior that uses the Model.beforeMarshal event and checks if user_id (or role, or whatever) is present and not empty and then run a check on all fields you want for the given user id or user role using ACL.
You could probably use this method PermissionsTable::check() or you can write a more dedicated method does checks on multiple objects (fields) at the same time. Like I said, you'll spend some time to figure the best way out using ACL if you go for it.
UX and yet another cheap solution
First I would not show fields at all an user is not allowed to change or enter as inputs. If you need to show them, fine, disable the form input or just show it as text. Then use a regular set of validation rules that requires the field to be empty (or not present) or empty a list of fields based on your users role. If you don't show the fields the user would have to temper the form and then fail the CSRF check as well (if used).
I don't think you need to validate in the table. I just thought of a way to do it in the controller.
In my Users/Add method in the controller for instance:
public function add()
{
$user = $this->Users->newEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->data);
//check if user is logged in and is a certain user
if ($this->request->session()->read('Auth.User.id') === 1) {
//allow adding/editing role or whatever
$user->role = $this->request->data('role');
} else {
$user->role = 4;//or whatever the correct data is for your problem.
}
if ($this->Users->save($user)) {
$this->Flash->success(__('You have been added.'));
} else {
$this->Flash->error(__('You could not be added. Please, try again.'));
}
}
$this->set(compact('user'));
$this->set('_serialize', ['user']);
}
If I want to create a simple mechanism such as the rights extension that serves limiting and checking the access rights to the action is that I have to do on each of my controller ie preFilter ?
For example in the case. I create a table with a set of user authentication in the table below :
1 = Allow and 0 = Not Allow
Every controller I have, if the user requests to the action it will always be checked whether the current user has permissions or not
I see here : http://www.yiiframework.com/doc/guide/1.1/en/basics.controller that this can be done
class PerformanceFilter extends CFilter
{
protected function preFilter($filterChain)
{
// logic being applied before the action is executed
return true; // false if the action should not be executed
}
protected function postFilter($filterChain)
{
// logic being applied after the action is executed
}
}
How do I order to be able to use it with the AR in Yii to be able to do this?
Or is there a better way for this case?
Does it help to not make the same code over and over again in every action to check the authorization of users that access is permitted or not
Thanks
I've been working with PHP for about a year, but I do it as a hobby. I dont have anybody I can go to as a teacher or a mentor to give me advice on what I may be doing completely wrong, or what I could do better. I've done quite a few different things within that year, so I wouldnt consider myself a complete noob.
Anyways, I have just started using a framework (Kohana), and there really arent that many tutorials out there, so I'm not entirely sure if I'm doing things in a good way.
I have a few code snippets that I would like to post to get some feedback pertaining to what I just said.
For Starters
User Controller
class User_Controller extends Template_Controller{
public function register()
{
// logged in users cant register
if($this->logged_in)
{
url::redirect('user/profile');
}
// initially show an empty form
$form = $errors = array
(
'username' => '',
'email' => '',
'password' => '',
'gender' => '',
'dob_month' => '',
'dob_day' => '',
'dob_year' => '',
'date_of_birth' => '',
'captcha' => '',
'registration' => ''
);
// check for a form submission
if($this->input->post('register'))
{
// get the form
$post = $this->input->post();
// prepare the data for validation
$post['date_of_birth'] = "{$post['dob_year']}-{$post['dob_month']}-{$post['dob_day']}";
// create a new user
$user = ORM::factory('user');
// validate and register the user.
if($user->register($post, TRUE))
{
// SEND EMAIL
// login using the collected data
if(Auth::instance()->login($post->username, $post->password, TRUE))
{
// redirect the user to the profile page
//url::redirect("user/profile/{$user->id}");
}
}
// get validation errors and repopulate the form
$form = arr::overwrite($form, $post->as_array());
$errors = arr::overwrite($errors, $post->errors('registration_errors'));
}
// template variables
$this->template->title = 'Sign Up';
$this->template->body = new View('layout_1');
// layout variables
$this->template->body->left = new View('user/registration_form');
$this->template->body->right = 'Right Side Content';
// registration form variables
$this->template->body->left->form = $form;
$this->template->body->left->errors = $errors;
$this->template->body->left->captcha = new Captcha('register');
}
}
Register Function within User_Model
class User_Model extends ORM{
public function register(array& $user, $save = FALSE)
{
$user = new Validation($user);
// logged in users cant register
if(Auth::instance()->logged_in())
{
$user->add_error('registration', 'logged_in');
return FALSE;
}
// trim everything
$user->pre_filter('trim')
// everything is required
->add_rules('*', 'required')
// username must be 5 - 30 alphanumeric characters and available
->add_rules('username', 'length[5,30]', 'valid::alpha_numeric', array($this, 'username_available'))
// email must be valid format and available
->add_rules('email', 'valid::email', array($this, 'email_available'))
// password must be 5 - 15 characters and alpha dash
->add_rules('password', 'length[5,15]', 'valid::alpha_dash')
// gender must be either male or female. capitalize first letter
->add_rules('gender', array($this, 'valid_gender'))
->post_filter('ucfirst', 'gender')
// dob must be a valid date, and user must be old enough.
->add_callbacks('date_of_birth', array($this, 'check_dob'))
// captcha must be entered correctly.
->add_rules('captcha', 'Captcha::valid');
// add the registration date
$this->registration_date = date::unix2mysql(); // helper function transforms the current unix to mysql datetime format
// validate the information. an ORM function.
$result = parent::validate($user, $save);
// was the user info valid?
if($result === TRUE)
{
// was the user saved?
if($save === TRUE)
{
// add a login role
$this->add(ORM::factory('role', 'login'));
$this->save();
}
}
else
{
$user->add_error('registration', 'failed');
}
return $result;
}
}
Mostly all my models follow the same format when validating info.
I have some other things I would appreciate feedback on as well, but I dont want to overwhelm anybody.
Thanks a lot for your time
EDIT: I'm sorry, I should've posted both the user controller and model. I've been reading alot about how models should be fat, and controllers should be skinny. Thats why I created a register function in the model to validate the info instead of doing so within the controller. The register function takes an array, but turns that array into a validation object so that I can retrieve the user input, and the errors. I've seen a few tutorials on Kohana where it was done this way.
First, I would not put the register() method into the User model. A model should be a representation of the object in the database and generally only contains your "CRUD" methods (create, retrieve, update, delete), getter and setter methods, and maybe some static helper methods related to the model. By putting your register() method into the model, you're making the model do presentation logic that should really be done by a User controller, since this is a response to a user action. A controller handles user actions, validates those user actions, then updates the model if the validation is successful.
In your example, the user is attempting to create a new account. He fills out a form and clicks submit. The form's POST action should point to a controller's method, like /user/register, and that method will use the Validation library to validate the form data sent by the user. Only if that data validates successfully should you create a User model instance, set the properties of that model to what the user input, and then use the model's save() method to save to the database. If the validation fails, you report the error to the user and you don't create a User model at all since you don't have a valid data set to create a model with yet.
Next, you are checking to see if the user is logged in. Again, this should be in the controller, not the model. Besides that, the user should not be able to get to this register process in the first place if he is already logged in. The controller method that creates the user registration form view should check to see if the user's logged in, and if he is, then he should be redirected to another page. Even if the user is playing tricks and manages to submit the form (maybe he logged in via another window while having the form open in an old window), your register method should check for that first and not create a $user Validation object yet.
I can see in your code that there are some confusing items based on your model set up. For example, you're passing the $user array into the method, which I presume is the form data. But you're using the "pass by reference" operator (&) which is unnecessary in PHP5 since all objects are now passed by reference. But after that you're recasting $user as a Validation object. Are you using the $user Validation object elsewhere and require it to be passed by reference? If so, that's another flaw in the logic as all of this processing needs to be in the controller and the $_POST values can be used directly in the controller instead of having to pass around a Validation object.
Later on, you're validating the user information with parent::validate($user, $save). Why is the validate() method being called on parent as a static method? If this is a model, it should be extending Kohana's core Model class, and "parent" references the Model class. Is your model extending the Validation class? Also, why are you passing in the $user Validation object to the validation() method? Doing that is required if you need to do recursion (to validate elements again after making changes from previous filters), but it looks like you're not doing anything to require recursion. You should be calling validate() on the $user Validation object:
$user->validate();
without any arguments. The validation errors will become part of the $user object, so you can check for errors using
$user->errors();
Finally, while Kohana allows you to use method chaining, I would not use one long chain to set up the rules and other items for the validation. It's confusing and may cause debugging to be difficult. Put each of those on its own line and perform each directly on the $user object.
I dont know Kohanna so im not sure what the lay of the land is on their MVC separation but typically i would make register an action on a controller. The main thing i disagee with in your code is that the Model is coupled to the Authentication system internally. The authentication check should be made outside the class and the control flow decision should be made outside as well, OR the result of the authentication check should be passed in to the Model for use in its internal operation.
Typically i might do something like the following pseudo code:
// in my controller class for User or whatever
public function registerAction()
{
// get the form data from the request if its POST, ortherwise a blank array
$userData = $this->getRequest('user', array(), 'POST');
// create a user
$user = new User($userData);
if(Auth::instance()->logged_in())
{
// we are logged in add an error to the user object for use by the view
$user->getValidator()->add_error('registration', 'logged_in');
}
elseif($user->is_valid())
{
// user data is valid, set the view with the success message
$user->save();
$this->setView('register_success');
}
/**
* render the designated view, by default this would be the one containing the
* registration form which displays errors if they exist - however if we success-
* fully registered then the view with the success message we set above will be
* displayed.
*/
$this->render();
}