This is a question about RBAC usage in Yii2.
So far I have found it to work rather well and satisfactory, however there is one key feature that I am missing: The ability for Yii2 Rules to provide "feedback" in a similar way as Yii2 Validators set Error messages to explain why the validation failed. I am looking for a way to provide some sort of feedback as to why the permission was not granted.
In particular, the can() method will return a boolean type, which is fine, but when checking for permission we have no idea why exactly the user was not granted that particular permission.
To give a more practical example. Let's say we are trying to determine whether the current user can submit a comment. We would typically do something like this:
if (Yii::$app->user->can('postComment',['comment'=>$comment])) {
$comment->post();
} else {
throw new ForbiddenHttpException('Sorry m8, you cant do this. No idea why tho!');
}
It works great, but as shown in the example we really have no idea why the user isn't able to post the comment. Could be any number of reasons, for example because the thread is locked or because they do not have permission to post in a certain category or because they dont have a high enough reputation etc. But we want to tell the user why! So my question is, how do we get that feedback from Yii2's RBAC?
You would want to create your own AccessRule and set the message exceptions from your scenarios by overriding the current methods in that class. matchRole would be the method you would be overriding. Yii2 doesn't have this in place so you would have to roll your own AccessRule to do so.
Then once its created attach it to your controllers:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'ruleConfig' => [
'class' => 'app\components\AccessRule'
],
'rules' => [
/* my normal rules */
],
],
];
}
So basically all I did was add
'message' => 'Current password cannot be blank.'
to my rules.
Make sure you seperate the correct rules, so you don't get that message on multiple fields, where it doesn't make sense. Also make sure you add it on the 'required' rule, unless you want that message to show when it's another rule..
I hope this helped you guys, as I spent a bit too much time searching for it.
The fastest way to get feedback from RBAC is to pass an object as a parameter. If the check fails then the error text is entered into this object. In this way, the reason for the negative test result can be obtained.
Let's say we want to prevent users from commenting on their posts.
Validation in Rule. This Rule is associated with postComment permission:
class PostCommentRule extends yii\rbac\Rule
{
public $name = 'PostCommentRuleName';
public function execute($user, $item, $params)
{
$allowed = $params['post']->owner_id !== $user;
if(!$allowed && isset($params['errorObject']))
$params['errorObject']->errorText = 'Comments to oneself are not allowed.';
return $allowed;
}
}
We check permission and in case of prohibition we have a reason:
$errorObject = new stdClass();
if (Yii::$app->user->can('postComment',['post'=>$post,'errorObject'=>$errorObject])) {
$comment->post();
} else {
throw new ForbiddenHttpException($errorObject->errorText);
}
In your case I would create a base permission class which will cover an abstraction for specific restriction message with one simple method and it will be extended by all your permissions.
This is the abstract permission blueprint.
abstract class AbstractPermission extends Permission
{
/**
* #return string
*/
abstract public function getRestrictionMessage(): string;
}
Creating custom database manager in order to check if retrieved permission has implement the abstraction.
class CustomDbManager extends DbManager
{
/**
* #throws \Exception
* #return AbstractPermission|null
*/
public function getPermission($name): ?AbstractPermission
{
$permission = parent::getPermission($name);
if ($permission === null) {
return null;
}
if (!$permission instanceof AbstractPermission) {
throw new \Exception(
'Your permission class should be derived from ' . AbstractPermission::class
);
}
return $permission;
}
}
Define CustomDbManager in your configuration file
'components' => [
'authManager' => [
'class' => CustomDbManager::class
],
...
];
Example with your PostCommentPermission.
class PostCommentPermission extends AbstractPermission
{
/**
* #return string
*/
public function getRestrictionMessage(): string
{
return 'You cannot post comments!';
}
}
And finally invoke your manager with specific permission check
$authManager = Yii::$app->getAuthManager();
$postCommentPermission = $authManager->getPermission('postComment');
if (Yii::$app->user->can($postCommentPermission->name, ['comment' => $comment])) {
$comment->post();
} else {
throw new ForbiddenHttpException($postCommentPermission->getRestrictionMessage());
}
Related
The attributes of laravel modal are named using underscore (_), for example :
first_name
but attributes of javascript objects are named with camelCase:
{ firstName: "..." }
And this presents a conflict, is there a solution to resolve it ?
Try to use Laravel eloquent resource pattern will do that for You.
Check this helpful documentation.
https://laravel.com/docs/8.x/eloquent-resources
Like Zrelli Mjdi mentioned it's done with Resource Collections.
I did not find a way to let this resources transform the result recursively for nested JSON-Objects, so I created a middleware (see the github-gist) for this, which should take a rather heavy toll on performance. So use it sparsely.
I'd use this middleware only temporary if your frontend demands camel-case properties. In the long run I'd modify my migrations to use camel-case fieldnames. This should, according to this reddit-thread, be possible and won't affect performance like my middleware.
Edit: The code in the gist had a bug which is now fixed.
This is about how it's done with Resource-Collections and non-nested JSON-Results:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class MyResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'userId' => $this->user_id,
'createdAt' => $this->created_at,
];
}
}
in the controller:
public function myControllerMethod(Request $request)
{
// ...
return MyResource::collection($logs)
}
Three weeks ago I was trying to find a way to send message (or notification) to admin after any user make create or update, but ended up with nothing. I searched a lot and I did not find a clear solution, I am trying to understand Yii2 events, I found this link
http://www.yiiframework.com/wiki/329/real-time-display-of-server-push-data-using-server-sent-events-sse/
I think it is the key to solve my problem, but I am really stuck I don't know what to do, hope anyone can help me.
thanks
Consider using a behavior to handle this.
Assumptions
You have at least one model (possibly multiple) within your project.
You have a controller that contains at least two actions: actionCreate and actionUpdate.
An email is sent to an administrator whenever either of the aforementioned actions are called.
Events and Behaviours
When actionCreate is called a new record is inserted into the database through an instance of a model class that extends ActiveRecord. Similarly, when actionUpdate is called an existing record is fetched from the database, updated and saved back. An event (i.e: insert or update) is fired by the model (since model extends component and components are responsible for implementing events) on both of these occasions. Yii2 provides the ability to respond to these events using behaviours which "customize the normal code execution of the component”.
In short, this means you can bind custom code to any given event such that your code executes when the event is fired.
Proposed Solution
Now that we know a little something about events and behaviours, we could create a behaviour that executes some custom code whenever an insert or an update event is fired. This custom code could check the name of the action being called (is it called create or update?) in order to determine whether an email is required to be sent out.
The behaviour is useless on it’s own though, we would need to attach it to any models that should be triggering it.
Implementation of Solution
NotificationBehavior.php
<?php
namespace app\components;
use yii\base\Behavior;
use yii\db\ActiveRecord;
class NotificationBehavior extends Behavior
{
/**
* Binds functions 'afterInsert' and 'afterUpdate' to their respective events.
*/
public function events()
{
return [
ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert',
ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate',
];
}
/**
* This function will be executed when an EVENT_AFTER_INSERT is fired
*/
public function afterInsert($event)
{
// check the 'id' (name) of the action
if (Yii::$app->controller->action->id === 'create') {
// send email to administrator 'user performed insert'
}
}
/**
* This function will be executed when an EVENT_AFTER_UPDATE is fired
*/
public function afterUpdate($event)
{
if (Yii::$app->controller->action->id === 'update') {
// send email to administrator 'user performed update'
}
}
}
PostController.php
<?php
namespace app\controllers;
use Yii;
use app\models\Post;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
class PostController extends Controller
{
/**
* Creates a new record
*/
public function actionCreate()
{
$model = new Post;
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
} else {
return $this->render('create', [
'model' => $model,
]);
}
}
/**
* Updates an existing record
*/
public function actionUpdate()
{
// ...
}
}
Post.php (model)
<?php
namespace app\models;
use app\components\NotificationBehavior;
use yii\db\ActiveRecord;
class Post extends ActiveRecord
{
/**
* specify any behaviours that should be tied to this model.
*/
public function behaviors()
{
return [
// anonymous behavior, behavior class name only
NotificationBehavior::className(),
];
}
}
I would also advise checking out Yii2's TimestampBehavior implementation for a more concrete example.
Do you have a model to "user"? If yes, then just override method afterSave (it fires exactly after making any changes in the model) like this:
public function beforeSave($insert)
{
if (parent::beforeSave($insert)) {
// your notification logic here
return true;
}
return false;
}
I am trying to use https://github.com/himiklab/yii2-recaptcha-widget for YII2 framework. Currently i am building a contact form, so i followed instruction there. However i faced a problem, by the instruction
public $reCaptcha;
public function rules()
{
return [
// ...
[['reCaptcha'], \himiklab\yii2\recaptcha\ReCaptchaValidator::className(), 'secret' => 'your secret key']
];
}
i have to add this in the model, but my contact form only exists in controller and view, i dont need a model to save the submission of feedback in database, so how can i do this verification of rules at controller layer?
You can try to use ad hoc validation for that.
$validator = new \himiklab\yii2\recaptcha\ReCaptchaValidator;
$validator->secret = '...';
if ($validator->validate($entered_recaptcha_code, $error)) {
// ok
} else {
echo $error;
}
I have not tried it before, some additional configuration might be needed.
Why authorize is to set to Controller while Authentication is done in AppController?
Like: I got it when Blog example is doing but did not get details explanation on it
$this->loadComponent('Auth', [
'authorize' => ['Controller']
]);
I read the Authorize Section but could not understand it. So Could someone please help making me understand it?
The book is describing how you would control your own authorization at the controller level.
Authentication identifies a valid user. If any logged in user may access any part of your app, then you don't need to implement any further authorization. But if you wish to restrict access to certain controllers, based on role for example, you could set 'authorize' => ['Controller'] in the Auth config as described, and then in each controller define your own isAuthorized() method, based on the role of the user.
For example, if in your InvoicesController you only want to let users whose role is 'Accounting' access the methods, you could include a test for that in an isAuthorized() method in the InvoicesController:
// src/Controller/InvoicesController.php
class InvoicesController extends AppController
{
public function isAuthorized($user)
{
if ($user['role'] === 'Accounting'){
return true;
}
return parent::isAuthorized($user);
}
// other methods
}
I would like to check if my user have filled certain fields in his profile before he can access any action of any controller.
For example
if(empty(field1) && empty(field2))
{
header("Location:/site/error")
}
In yii1 I could do it in protected\components\Controller.php in init() function
But in yii2 I'm not sure where to put my code. I cannot modify core files, but not sure what to do in backend of my advanced application to make it work.
I know I can user beforeAction() but I have too many controllers to do that and to keep track of every controller
In case you need to execute a code before every controller and action, you can do like below:
1 - Add a component into your components directory, for example(MyGlobalClass):
namespace app\components;
class MyGlobalClass extends \yii\base\Component{
public function init() {
echo "Hi";
parent::init();
}
}
2 - Add MyGlobalClass component into your components array in config file:
'components' => [
'MyGlobalClass'=>[
'class'=>'app\components\MyGlobalClass'
],
//other components
3 - Add MyGlobalClass into bootstarp array in config file:
'bootstrap' => ['log','MyGlobalClass'],
Now, you can see Hi before every action.
Please note that, if you do not need to use Events and Behaviors you can use \yii\base\Object instead of \yii\base\Component
Just add in config file into $config array:
'on beforeAction' => function ($event) {
echo "Hello";
},
Create a new controller
namespace backend\components;
class Controller extends \yii\web\Controller {
public function beforeAction($event)
{
..............
return parent::beforeAction($event);
}
}
All your controllers should now extend backend\components\Controller and not \yii\web\Controller. with this, you should modify every controller. I would go for this solution.
I believe you might also replace 1 class with another (so no change to any controller necessary), something like
\Yii::$classMap = array_merge(\Yii::$classMap,[
'\yii\web\Controller'=>'backend\components\Controller',
]);
See more details here: http://www.yiiframework.com/doc-2.0/guide-tutorial-yii-integration.html and I took the code from here: https://github.com/mithun12000/adminUI/blob/master/src/AdminUiBootstrap.php
you can put this in your index.php file. However, make sure you document this change very well as somebody that will come and try to debug your code will be totally confused by this.
Just i think this code on config file can help you:
'on beforeAction' => function ($event) {
// To log all request information
},
'components' => [
'response' => [
'on beforeSend' => function($event) {
// To log all response information
},
],
];
Or, https://github.com/yiisoft/yii2/blob/master/docs/guide/security-authorization.md use RBAC, to restrict access to controllers actions one at a time based on rules. Why would you want to restrict access to controller actions based on user fields is beyond me. You will not be able to access anything (including the login form) if you put a restriction there.