RBAC Yii2 doesn't work with default roles - php

I'm following the Definitive Guide to Yii 2.0. In my application I have two roles: the admin, who can do everything and the viewer, who can do some actions that unregistered users can't do. I'm trying to use default roles functionality of Yii 2 RBAC, but it seems doesn't work. The user table in my database has a column named "role": for admin it's value set to 1 and for viewers = 2.
What I did:
/app/rbac/UserGroupRule.php
namespace app\rbac;
use Yii;
use yii\rbac\Rule;
class UserGroupRule extends Rule {
public $name = 'userGroup';
public function execute($user, $item, $params) {
if (!Yii::$app->user->isGuest) {
$group = Yii::$app->user->identity->role;
if ($item->name === 'admin') {
return $group == 1;
} elseif ($item->name === 'viewer') {
return $group == 1 || $group == 2;
}
}
return false;
}
}
$auth = Yii::$app->authManager;
$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);
$author = $auth->createRole('viewer');
$author->ruleName = $rule->name;
$auth->add($viewer);
$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
$auth->addChild($admin, $viewer);
in my controller:
public function behaviors() {
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['admin'],
'rules' => [
[
'allow' => true,
'actions' => ['admin'],
'roles' => ['admin'],
],
],
],
];
}
When I try to access "admin" action, it says Forbidden #403, even when I'm an admin. How to make it work?

The user table in my database has a column named "role": for admin it's value set to 1 and for viewers = 2
That's not how it works unfortunately.
The rights/roles a user has are (by default) done via the auth_assignment-table.
Just add an entry in it:
INSERT INTO `auth_assignment` VALUES ("admin", <user-id>, NOW());
(be sure to change the user ID into whatever user you want to make admin.
That should solve your issue.
Edit (as I misread some of your question):
As per this link you can indeed define default roles, but you have to make sure to also reconfigure your authManager-component in the configuration file to include the default roles:
'components' => [
'authManager' => [
// ...
'defaultRoles' => ['admin', 'viewer'],
],
],
This list of roles indicate the permissions that always should be checked for every user, no matter if they have an entry in the auth_assignment-table or not.

I was facing the same issue with op. Finally made it work after tinkering with xdebug for a while.
I feel the official documentation on default roles is missing a couple important points, I will summarize them below with some of my personal experiences. The project structure is based on Yii 2.0 Advanced Project Template
Database
user table contains id and group. Where group is type int, 1 for admin and 2 for author
Rules setup
Code simplified for clarity.
The rule class, where you put the actual rule logic.
yii/console/controller/UserGroupRule.php
namespace app\rbac;
use Yii;
use yii\rbac\Rule;
/**
* Checks if user group matches
*/
class UserGroupRule extends Rule
{
public $name = 'userGroup';
public function execute($user, $item, $params)
{
if (!Yii::$app->user->isGuest) {
$group = Yii::$app->user->identity->group;
if ($item->name === 'admin') {
return $group == 1;
} elseif ($item->name === 'author') {
return $group == 1 || $group == 2;
}
}
return false;
}
}
Now defining the roles..
yii/console/controller/RbacController.php
namespace console\controllers;
use Yii;
use yii\console\Controller;
class RbacController extends Controller
{
public function actionInit()
{
$auth = Yii::$app->authManager;
$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);
$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
// define 'author' here...
}
}
After you have this file ready, you should be able to run ./yii rbac/init to generate the rule files:
console/rbac/items.php
console/rbac/rules.php
Important: You need to place the generated files under your desired application folder, this is crucial. Other wise Yii 2.0 will not be able to pick up the rules. For example: yii/backend/rbac/
Controller and config setup
This is mostly identical to the documentation
yii/commom/config/main.php
Add the following to the return array:
'authManager' => [
'class' => 'yii\rbac\PhpManager',
'defaultRoles' => ['admin', 'author'], // your define roles
],
Now the fun part, under the controller class you would like to apply the rules
yii/backend/controllers/SiteController.php
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'actions' => [], // applies to all actions
'roles' => ['admin'], // your defined roles
],
],
],
Up to this point, the rules should be working. Under your controller class, double check Yii::$app->getAuthManager() see if it contains your defined roles. If not, it means Yii did not pick up the rules correctly, please check previous steps again.

Related

Custom DatabaseSessionHandler in Laravel 8

I'm trying to make a custom DatabaseSessionHandler but it doesn't work as expected.
The idea is to make the table sessions polymorphic to allow the session from multiple models.
(To be honest, I'm not even sure it's possible)
But even before changing the table to be polymorphic, I'm trying to add a custom driver in order to manipulate the sessions.
The issue seems that my DatabaseSessionHandler, is not correctly called when I try to sign in.
config/auth.php
'guards' => [
'web' => [
'driver' => 'custom-session',
'provider' => 'users',
],
'screen' => [
'driver' => 'custom-session',
'provider' => 'screens',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => Domain\User\Models\User::class,
],
'screens' => [
'driver' => 'eloquent',
'model' => Domain\Screen\Models\Screen::class,
],
]
AuthServiceProvider.php
public function boot()
{
//This is how the session is normally registered: https://github.com/laravel/framework/blob/8.x/src/Illuminate/Session/SessionManager.php#L83
Session::resolved(function ($session) {
$session->extend('screen-session', function ($app) {
$table = $app['config']['session.table'];
$lifetime = $app['config']['session.lifetime'];
$connection = $app['db']->connection($app['config']['session.connection']);
return new \Support\Session\DatabaseSessionHandler($connection, $table, $lifetime, $app);
});
});
// This is how the driver "session" is normally registered: https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/AuthManager.php#L121
Auth::resolved(function ($auth) {
$auth->extend('custom-session', function ($app, $name, array $config) {
$provider = Auth::createUserProvider($config['provider']);
$guard = new SessionGuard($name, $provider, $app->session->driver('screen-session'));
if (method_exists($guard, 'setCookieJar')) {
$guard->setCookieJar($this->app['cookie']);
}
if (method_exists($guard, 'setDispatcher')) {
$guard->setDispatcher($this->app['events']);
}
if (method_exists($guard, 'setRequest')) {
$guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
}
return $guard;
});
});
}
DatabaseSessionHandler.php
This is currently a copy/past of the existing one that is used by session except the namespace. https://github.com/laravel/framework/blob/8.x/src/Illuminate/Session/DatabaseSessionHandler.php
I don't have any error message.
When I try to sign in (auth()->guard('web')->login($user))
It validate the login
I know it uses my DatabaseSessionHandler to destroy the current
session in the table.
The session ID is regenerated and I'm not logged in
An another scenario;
I try to sign in with the "remember me"
It validate the login
Somehow, the session is updated in the database with the current user_id but it doesn't seems that my DatabaseSessionHandler have been used
I'm logged in
Removing the screen from the guards and providers to only keep web and users, doesn't change anything.
Finally solved it. As always, it's a simple mistake.
I add to change in .env file
SESSION_DRIVER=screen-session
And now it's working as expected.
Now, also to answer my another question
The idea is to make the table sessions polymorphic to allow the session from multiple models. (To be honest, I'm not even sure it's possible)
From my quick tests it looks realizable. Here's what I did;
create_sessions_table.php
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->nullableMorphs('authenticable');
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->text('payload');
$table->integer('last_activity')->index();
});
DatabaseSessionHandler.php
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Session\DatabaseSessionHandler as BaseDatabaseSessionHandler;
class DatabaseSessionHandler extends BaseDatabaseSessionHandler
{
/**
* Get the currently authenticated user's type.
*
* #return mixed
*/
protected function userType()
{
$user = $this->container->make(Guard::class)->user();
return optional($user)->getMorphClass();
}
/**
* Add the user information to the session payload.
*
* #param array $payload
* #return $this
*/
protected function addUserInformation(&$payload)
{
if ($this->container->bound(Guard::class)) {
$payload['authenticable_id'] = $this->userId();
$payload['authenticable_type'] = $this->userType();
}
return $this;
}
}
Note: If you're using Laravel jetstream, you will probably need a custom LogoutOtherBrowserSessionsForm livewire/inertiajs component since this one is based and hardcoded with the database session and looks for user_id column

How to avoid duplication in Laravel validation rules

For validating form validation rules I currently stored them in User Model and use it in Register Controller, User controller in admin panel, User Controller in APIs and some other places, but currently it's very hard to maintain because each controller needs a slightly different set of rules and when I change the rules in User Model other controllers will not work anymore. So how to avoid duplication in rules and still keep the code maintainable?
Approach I often use is to write a HasRules trait for my models, it looks something like this:
trait HasRules
{
public static function getValidationRules(): array
{
if (! property_exists(static::class, 'rules')) {
return [];
}
if (func_num_args() === 0) {
return static::$rules;
}
if (func_num_args() === 1 && is_string(func_get_arg(0))) {
return array_get(static::$rules, func_get_arg(0), []);
}
$attributes = func_num_args() === 1 && is_array(func_get_arg(0))
? func_get_arg(0)
: func_get_args();
return array_only(static::$rules, $attributes);
}
}
Looks messy, but what it does is allows you to retrieve your rules (from a static field if such exists) in a variety of ways. So in your model you can:
class User extends Model
{
use HasRules;
public static $rules = [
'name' => ['required'],
'age' => ['min:16']
];
...
}
Then in your validation (for example, in your FormRequest's rules() method or in your controllers when preparing rules array) you can call this getValidationRules() in variety of ways:
$allRules = User::getValidationRules(); // if called with no parameters all rules will be returned.
$onlySomeRules = [
'controller_specific_field' => ['required'],
'name' => User::getValidationRules('name'); // if called with one string parameter only rules for that attribute will be returned.
];
$multipleSomeRules = User::getValidationRules('name', 'age'); // will return array of rules for specified attributes.
// You can also call it with array as first parameter:
$multipleSomeRules2 = User::getValidationRules(['name', 'age']);
Don't be afraid to write some code for generating your custom controller specific rules. Use array_merge and other helpers, implement your own (for example, a helper that adds 'required' value to array if it's not there or removes it etc). I strongly encourage you to use FormRequest classes to encapsulate that logic though.
You can try using laravel's validation laravel documentation
it is really easy to use and maintain just follow these steps:
run artisan command: php artisan make:request StoreYourModelName
which will create a file in App/Http/Requests
in the authorize function set it to:
public function authorize()
{
return true;
}
then write your validation logic in the rules function:
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
Custom error messages add this below your rules function:
public function messages()
{
return [
'title.required' => 'A title is required',
'body.required' => 'A message is required',
];
}
Lastly to use this in your controller just add it as a parameter in your function.
public function create(Request $request, StoreYourModelName $storeYourModelName)
{
//
}
and that's all you need to do this will validate on form submission if validation passes it will go to your controller, keep in mind your validation logic does not have to be like mine thought i would show you one way that it can be done..

Admin LTE with Laratrust

I'm using Laratrust and try implements:
I created these files
App\MyMenuFilter.php
<?php
namespace App;
use JeroenNoten\LaravelAdminLte\Menu\Builder;
use JeroenNoten\LaravelAdminLte\Menu\Filters\FilterInterface;
class MyMenuFilter implements FilterInterface
{
public function transform($item, Builder $builder)
{
if (isset($item['permission']) && Laratrust::can($item['permission'])) {
return false;
}
return $item;
}
}
I changed this
config\adminlte.php
'menu' => [
'MAIN NAVIGATION',
[
'text' => 'Blog',
'url' => 'admin/blog',
'permission' => 'create-post', // Here
],
#code
]
'filters' => [
#code
//JeroenNoten\LaravelAdminLte\Menu\Filters\GateFilter::class,
App\MyMenuFilter::class,
],
But show this error:
Class 'App\Laratrust' not found (View: /var/www/html/multi-auth/vendor/jeroennoten/laravel-adminlte/resources/views/page.blade.php)
Simplest way
if (isset($item['permission']) && \Laratrust::can($item['permission'])) {
return false;
}
or you can
use Laratrust;
if (isset($item['permission']) && Laratrust::can($item['permission'])) {
return false;
}
the use statement should be right at the top before class definition and the condition will obviously be in your method
It appears that Laratrust is a package residing in vendor directory thats why trying to get it under the \App location will throw an error.
You dont import the App\Laratrust in your namespace. Add use App\Laratrust; to the top of your class and it should work.

frozennode administrator relationship restrict by tenant

I have three Laravel models within a multitenanted app:
Tenant {}
Company {
public function tenant() {
return $this->belongsTo('Tenant');
}
public function group() {
return $this->belongsTo('Group');
}
}
Group {
public function companies() {
return $this->hasMany('Company');
}
public function tenant() {
return $this->belongsTo('Tenant');
}
}
I am building an admin interface for my app using Frozennode Administrator, so the super admin can change stuff and so on. I want to be able to change the group of a company:
'edit_fields' => [
],
'group' => [
'title' => 'Group',
'type' => 'relationship',
'name_field' => "title",
//'constraints' => ['tenant' => 'tenant']
]
]
But my constraint does not work. How can I limit the groups shown to those matching the tenant that the company belongs to?
This is relatively simple. Just define a new relationship in your Company model like so: (and use this instead of "group" in your administrator config file):
public function tenant_group() {
return $this->belongsTo('Group')->whereTenantId(Session::get('current_tenant_id'));
}
The above code assumes you have a session variable called "current_tenant_id" which determines the current users' tenant. You can replace that with something else, perhaps something like this:
Auth::getUser()['tenant_id']

Difference between Controller and Model in MVC

I'm little confused about controller and model in MVC framework (codeIgniter). Its clear to me that controller methods calls the views and Model methods interact with database.
However, I'm little confused about the following types of methods, which are called by methods in a controller.
hash_password //returns hash password.
valid_email //validates email format and return true or false
is_logged //check if session has a variable, returns true or false
generate_random_string //generates and hashes a random string
Should they be placed in controller or in a model?
Currently I place all of the above functions in a controller. Is it correct?
I think the is_logged should be placed in the Model for User. Note that the User might be a customer in your case or any class that you have made to model a user of your service.
The valid_email and generate_random_string are more or less utility functions, which you can place in a Utility or Utilities model, so that these are reusable in various controllers in your application.
The hash_password, can be placed in either the User model or Utility model. I am more tempted to place it in Utility model, since its a hashing function and there is nothing the user cares about. However, I can imagine there can be argument(s) otherwise.
The following SO question (though for a different framework) can also serve as a rule of thumb:
Where to put custom functions in Zend Framework 1.10
generally controllers are used to determine how to handle the http requests made..
There's nothing wrong in creating some functions which directly respond to the http requests.
but if it has anything to do with the DB, its better to place those function in the model, and call them from the controller.
Controller should combine view with model, so every validation shoulde be placed in model
this is my example from kohana
CONTROLLER
<?php
/**
* User Controller
*/
class Controller_Admin_User extends Controller_Admin_Template {
public function action_index()
{
$this->template->body = View::factory('admin/user/index')
->set('i', 0)
->bind('users', $users)
->bind('groups', $groups)
->bind('id_user_group', $id_user_group);
$model_user = new Model_Admin_User;
$users = $model_user->get_users(Arr::get($_GET, 'sort'), Arr::get($_GET, 'order'));
$model_usergroup = new Model_Admin_Usergroup;
$groups = $model_usergroup->get_user_group();
}
public function action_add()
{
$this->template->body = View::factory('admin/user/form_add')
->bind('error', $error)
->bind('groups', $groups)
->bind('post', $post);
$model_usergroup = new Model_Admin_Usergroup;
$groups = $model_usergroup->get_user_group();
if($_POST)
{
$model_user = new Model_Admin_User;
if($model_user->save($_POST) == false)
{
$error = $model_user->error;
$post = $_POST;
}
else
{
$this->request->redirect('admin/user');
}
}
}
MODEL
class Model_Back_User extends Model {
private $qb;
public $aliases = array(
'id'=> 'id_user'
);
public $error = array(
'name' => null,
'surname' => null,
'login' => null,
'password' => null,
'id_user_group' => null,
'old_password' => null,
'new_password' => null,
'confirm' => null,
'email' => null,
'phone' => null,
);
private $rules = array(
'name' => array('not_empty' => null, 'alpha' => null),
'surname' => array('not_empty' => null, 'alpha' => null),
'login' => array('not_empty' => null),
'password' => array('not_empty' => null),
'id_user_group' => array('not_empty' => null),
'email' => array('not_empty' => null, 'email' => null),
'phone' => array('not_empty' => null),
'old_password' => array('not_empty' => null),
'new_password' => array('not_empty' => null),
'confirm' => array('matches' => array('new_password'))
);
public function __construct()
{
$this->qb = new Querybuilder;
//parent::__construct();
}
public function change_password($data)
{
$validate = Validate::factory($data)
->filter(true, 'trim')
->rules('old_password', $this->rules['old_password'])
->rules('new_password', $this->rules['new_password'])
->rules('confirm', $this->rules['confirm'])
->callback('old_password', array($this, 'password_exists'), array('id_user'=> $data['id_user']));
if($validate->check() == false)
{
$this->error = array_merge($this->error, $validate->errors('user'));
return false;
}
$u = Session::instance()->get('user');
$this->edit(array('password'=> $this->password($data['new_password'])), array('id_user'=> $u['id_user']));
return true;
}
public function password_exists(Validate $valid, $field, $param)
{
if($this->user_exists(array('password'=> $this->password($valid[$field]), 'id_user'=> $param['id_user'])) == false)
{
$valid->error($field, 'old password is incorrect', array($valid[$field]));
}
}
public function save($data)
{
$validate = Validate::factory($data)
->filter(true, 'trim')
->rules('name', $this->rules['name'])
->rules('surname', $this->rules['surname'])
->rules('user_group_id', $this->rules['id_user_group'])
->rules('email', $this->rules['email'])
->rules('phone', $this->rules['phone']);
$edit = false;
if(isset($data['id_user']) AND Validate::not_empty($data['id_user']))
{
$edit = true;
}
else
{
$validate->rules('login', $this->rules['login'])
->rules('password', $this->rules['password']);
}
if($validate->check() == false)
{
$this->error = array_merge($this->error, $validate->errors('user'));
return false;
}
if($edit == true)
{
$this->edit(
array(
'name' => $data['name'],
'user_group_id' => $data['user_group_id']
),
array(
'id_user'=> $data['id_user']
)
);
return true;
}
return $this->add(
array(
'name' => $data['name'],
'login' => $data['login'],
'password' => $data['password'],
'user_group_id' => $data['user_group_id']
)
);
}
protected function add($data)
{
$data['password'] = $this->password($data['password']);
return $this->_db->query(Database::INSERT,
$this->qb->insert('user')->set($data)->build_query()
);
}
View is not so important thats why i dont put this here.
Generally speaking - a model should know stuff about it's own data. So anything related purely to a model's own data - should go in the model.
Eg the hash_password and email-validation methods - a model should know how to validate or update it's own data-fields, so those should go in the model.
However a controller should know about how to direct user actions appropriately and to load the correct models for views etc.
EG the session-related method should go in the controller, because the session is used for storing the user's state (based on past actions).
The "generate random string" method is very vague and may be used everywhere. I'd put that in a separate library possibly included in the model/controller as appropriate.
I've been using Codeigniter for a long time and I'd do the following with your functions as far as placement goes:
hash_password //returns hash password.
I'd put something like a password hasher in a library or helper file so I could call it from my controller like:
// pretend library I'd make for tasks like hashing etc
$this->load->library('passwords');
// transform posted password into it's hashed version
$password = $this->password_library->hash_password($this->input->post('password'));
I'm assuming you want to hash/salt the password and store it in your database in that example
valid_email //validates email format and return true or false
This is already in form_validation, so...
is_logged //check if session has a variable, returns true or false
This should also connect to a authentication library
generate_random_string //generates and hashes a random string
Again, this would come from a library or helper.
SO WHEN DO YOU USE A MODEL?
Me, I use models exclusively for in/out on the database. All my queries go in there. I usually have my model's functions return data objects so I can loop through them in my views.
Controllers call your data from your models, then dump everything into your views. Outside functionality always goes into libraries and helpers. I like to do the "MY_library" and extend Codeigniter's own stuff - especially with forms and the html helper etc.

Categories