does anyone know a way of mapping the controller methods with permissions authorisation?
Let's say that I have 20 controllers, with index,store,show and delete methods and I don't wanna put in each method of this controller the correspondent permission, just for the sake of ... DRY.
What I wanna do instead is trying to map the permissions with controller actions.
An example would be:
https://laravel.com/docs/5.5/authorization#writing-gates
Gate::resource('posts', 'PostPolicy');
This is identical to manually defining the following Gate definitions:
Gate::define('posts.view', 'PostPolicy#view');
Gate::define('posts.create', 'PostPolicy#create');
Gate::define('posts.update', 'PostPolicy#update');
Gate::define('posts.delete', 'PostPolicy#delete');
for me something like this would fit:
Permission::map('route', 'permission');
Permission::map('users.store', 'create-user');
or even better
Permission::mapResource('users', '????');
I created a Trait for that, if you have a better suggestion please.
namespace App\Traits;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Pluralizer;
use Spatie\Permission\Exceptions\UnauthorizedException;
trait Authorisation
{
private $permissions = [
'index' => 'view',
'store' => 'create',
'show' => 'view',
'update' => 'edit',
'destroy' => 'delete'
];
private $action;
public function callAction($method, $parameters)
{
$permission = $this->getPermission($method);
if(($permission && Auth::user()->can($permission)) || !$permission)
return parent::callAction($method, $parameters);
if(Request::ajax()) {
return response()->json([
'response' => str_slug($permission.'_not_allowed', '_')
], 403);
}
throw UnauthorizedException::forPermissions([$permission]);
}
public function getPermission($method)
{
if(!$this->action = array_get($this->getPermissions(), $method)) return null;
return $this->routeName() ? $this->actionRoute() : $this->action;
}
public function registerActionPermission($action, $permission) {
$this->permissions[$action] = $permission;
}
private function actionRoute() {
return Pluralizer::singular($this->action . '-' . $this->routeName());
}
private function routeName() {
return explode('.', Request::route()->getName())[0];
}
private function getPermissions()
{
return $this->permissions;
}
}
And use it in controller like:
use Authorisation;
and if a want a custom permission for an action which does not exist in the $permissions:
$this->registerActionPermission('action_name', 'action-permission');
Related
Please help me in fixing this problem. I want to try sizeg/yii2-jwt (https://github.com/sizeg/yii2-jwt). I followed the Step-by-step usage example but I always get authorization issues. I also want to change the Model (I want to replace it with something other than the User model).
On Github it says after installing the plugin I have to edit web.php
'jwt' => [
'class' => \sizeg\jwt\Jwt::class,
'key' => 'secret',
'jwtValidationData' => \app\components\JwtValidationData::class,
],
After that I should create JwtValidationData class. where you have to configure ValidationData informing all claims you want to validate the token:
class JwtValidationData extends \sizeg\jwt\JwtValidationData
{
/**
* #inheritdoc
*/
public function init()
{
$this->validationData->setIssuer('');
$this->validationData->setAudience('');
$this->validationData->setId('4f1g23a12aa');
parent::init();
}
}
in the User model:
public static function findIdentityByAccessToken($token, $type = null)
{
foreach (self::$users as $user) {
if ($user['id'] === (string) $token->getClaim('uid')) {
return new static($user);
}
}
return null;
}
And the controller:
class ProfileController extends Controller {
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => JwtHttpBearerAuth::class,
'optional' => [
'login',
],
];
return $behaviors;
}
private function generateJwt($id) {
$jwt = Yii::$app->jwt;
$signer = $jwt->getSigner('HS256');
$key = $jwt->getKey();
$time = time();
return $jwt->getBuilder()
->issuedBy('')
->permittedFor('')
->identifiedBy('4f1g23a12aa', true)
->issuedAt($time)
->expiresAt($time + 3600)
->withClaim('uid', $id)
->getToken($signer, $key);
}
public function actionLogin($person_id)
{
$token = $this->generateJwt($person_id);
return $this->asJson([
'id' => $token->getClaim('uid'),
'token' => (string) $token
]);
}
public function actionData()
{
return $this->asJson([
'success' => true
]);
}
}
I thought it was the same as the tutorial but I always get unauthorized. How to solve this problem?
You just created a token for the user, but where you use that?
you have to send token as "Bearer" authentication in your header to achieve this goal if you want to authenticate the user by "JwtHttpBearerAuth" behavior.
otherwise, you have to login the user manually in your code.
class LoginController extends Controller
{
#overwrite trait AuthenticatesUsers->credentials
protected function credentials(Request $request)
{
return array_merge( $request->only($this->username(), 'password'), ['active' => 1]);
}
}
class ForgotPasswordController extends Controller
{
//try to overwrite here
}
\vendor\laravel\framework\src\Illuminate\Auth\Passwords\PasswordBroker.php
class PasswordBroker implements PasswordBrokerContract
{
if (is_null($user) || $user->active != 1) { <-- add $user->active != 1
return static::INVALID_USER;
}
}
I custom Laravel register/login system by added email 'active' column.
In my login controller, I overwrite trait to check active column, but I having trouble on reset password.
What I did now is I add $user->active !=1 into PaswordBroker and it works fine, but
I don't want to touch vendor's files and I wish to overwrite it in my controller.
anyone know how to achieve this?
It is done within the sendResetLinkEmail of the class, override like so:
public function sendResetLinkEmail(Request $request)
{
$this->validate($request, [
'email' => 'required|email',
]);
$response = $this->broker()->sendResetLink([
'email' => $request->input('email'),
'active' => true,
]);
return $response == Password::RESET_LINK_SENT
? $this->sendResetLinkResponse($response)
: $this->sendResetLinkFailedResponse($request, $response);
}
If you want that, try to re-init a new class extends PasswordBroker, after that try to overrite method (add your condition to ...that method).
class YourClass extends PasswordBroker
{
public function sendResetLink(array $credentials, Closure $callback = null)
{
$user = $this->getUser($credentials);
if (is_null($user) || $user->active != 1) {
return PasswordBrokerContract::INVALID_USER;
}
$token = $this->tokens->create($user);
$this->emailResetLink($user, $token, $callback);
return PasswordBrokerContract::RESET_LINK_SENT;
}
}
I don't know your Laravel version are you using, I'm using 5.2.
Hope this help!
I created custom actions for rest api in yii2
my codes are:
namespace app\controllers;
use yii\rest\ActiveController;
use yii\web\Response;
use Yii;
class RsController extends ActiveController{
public $modelClass='app\models\Mymodel';
/*some another actions*/
public function actionOne($id){
return \app\models\Anothermodel::findAll(['my_id'=>$id]);
}
public function actionTwo($id){
return \app\models\Anothermodel::findAll(['my_name'=>'xxxx']);
}
}
I know we can override fields function in model to get special fields but
now I wanted to get different fields for actionOne and actionTwo (of a model)
How can I override fields function in Anothermodel for this purpose?
I found my answer from here
I create a component like this
<?php
namespace app\components;
class Serializer extends \yii\rest\Serializer {
public $defaultFields;
public $defaultExpand;
public function init() {
parent::init();
$this->defaultFields = !is_null($this->defaultFields) ? implode(",", $this->defaultFields) : $this->defaultFields;
$this->defaultExpand = !is_null($this->defaultExpand) ? implode(",", $this->defaultExpand) : $this->defaultExpand;
}
protected function getRequestedFields() {
$fields = is_null($this->request->get($this->fieldsParam)) ? $this->defaultFields : $this->request->get($this->fieldsParam);
$expand = is_null($this->request->get($this->expandParam)) ? $this->defaultExpand : $this->request->get($this->expandParam);
return [
preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY),
preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY),
];
}
}
and then in my controllers action set my fields
like this.
public function actionOne($id){
$this->serializer['defaultFields'] = ["field1",
"field2"];
return new \yii\data\ActiveDataProvider([
'query' => \app\models\Anothermodel::find()->where(['my_id'=>$id]),
]);
}
public function actionTwo($id){
$this->serializer['defaultFields'] = ["field1",
"field2","field3"];
return new \yii\data\ActiveDataProvider([
'query' => \app\models\Anothermodel::find()->where(['my_id'=>$id]),
]);
}
I suggest to use events
public function actionPublic()
{
\yii\base\Event::on(Thing::class, Thing::EVENT_AFTER_FIND, function ($event) {
$event->sender->scenario = Thing::SCENARIO_SEARCH_PUBLIC;
});
return new ActiveDataProvider([
'query' => Thing::find(),
]);
}
public function actionPrivate()
{
\yii\base\Event::on(Thing::class, Thing::EVENT_AFTER_FIND, function ($event) {
$event->sender->scenario = Thing::SCENARIO_SEARCH_PRIVATE;
});
return new ActiveDataProvider([
'query' => Thing::find(),
]);
}
and inside of ActiveRecord (Thing in my case) check the scenario in fields() method
public function fields()
{
$fields = parent::fields();
if ($this->scenario === self::SCENARIO_SEARCH_PUBLIC) {
unset($fields['field1'], $fields['field2'], $fields['field3'], $fields['field4']);
}
return $fields;
}
check my answer in gihub
I am using a repository pattern in my Laravel 4 project but come across something which I think I am doing incorrectly.
I am doing user validation, before saving a new user.
I have one method in my controller for this:
public function addNewUser() {
$validation = $this->userCreator->validateUser($input);
if ( $validation['success'] === false )
{
return Redirect::back()
->withErrors($validation['errors'])
->withInput($input);
}
return $this->userCreator->saveUser($input);
}
Then the validateUser method is:
public function validate($input) {
$rules = array(
'first_name' => 'required',
'last_name' => 'required',
'email_address' => 'unique:users'
);
$messages = [
];
$validation = Validator::make($input, $rules, $messages);
if ($validation->fails())
{
$failed = $validation->messages();
$response = ['success' => false, 'errors' => $failed];
return $response;
}
$response = ['success' => true];
return $response;
}
This may be okay, but I dont like doing the if statement in my controller? I would rather be able to handle that in my validation class.
But to be able to redirect from the validation class, I need to return the method in the controller.
What if I then want to have 5 methods called, I cant return them all?
I would like to be able to simply call the methods in order, then in their respective class handle what I need to and if there is any errors redirect or deal with them. But if everything is okay, simply ignore it and move to the next function.
So example:
public function addNewUser()
{
$this->userCreator->validateUser($input);
$this->userCreator->formatInput($input);
$this->userCreator->sendEmails($input);
return $this->userCreator->saveUser($input);
}
If doing the if statement in the controller isn't as bad as I think then I can continue, but this seems incorrect?
For repository pattern, you can use this :-
setup your basemodel like this
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class BaseModel extends Model{
protected static $rules=null;
protected $errors=null;
public function validateForCreation($data)
{
$validation=\Validator::make($data,static::$rules);
if($validation->fails())
{
$this->errors=$validation->messages();
return false;
}
return true;
}
/**
* #return errors
*/
public function getErrors() { return $this->errors; }
}
now in your repository, add these methods
protected $model;
protected $errors=null;
public function model(){ return $this->model; }
public function getErrors(){ return $this->errors; }
public function create($inputs)
{
if(!$this->model->validateForCreation($inputs))
{
$this->errors=$this->model->getErrors();
return false;
}
$new=$this->model->create($inputs);
return $new;
}
and the controller will look like this..
public function postCreate(Request $request)
{
$inputs=$request->all();
if($new=$this->repo->create($inputs))
{
return redirect()->back()
->with('flash_message','Created Successfully');
}
return redirect()->back()->withInput()->withErrors($this->repo->getErrors())
->with('flash_message','Whoops! there is some problem with your input.');
}
I have the following piece of code:
if(Request::ajax())
{
$response_values = array(
'validation_failed' => 1,
'errors' => $validator->errors->toArray()
);
return Response::json($response_values);
}
else
{
return Redirect::route("resource.create")
->withInput()
->withErrors($validator->errors);
}
I have this a lot in my code, and would like to find a way to automate this.
I tried creating a method in BaseController but it doesn't redirect properly, I also tried an after filter, but I was unable to pass parameters to this after filter, as I would need to pass errors and route.
How could I achieve this?
This is not working for you?
class BaseController extends \Controller {
public function processAndRedirectError($validator)
{
if(Request::ajax())
{
$response_values = array(
'validation_failed' => 1,
'errors' => $validator->errors->toArray()
);
return Response::json($response_values);
}
else
{
return Redirect::route("resource.create")
->withInput()
->withErrors($validator->errors);
}
}
}
class MyController extends BaseController {
public function store()
{
$validator = Validator::make(...);
return $this->processAndRedirectError($validator);
}
}