I created a policy in laravel 5.3 with this two actions:
class ProjectPolicy {
...
public function index(User $user)
{
return true;
}
public function create(User $user)
{
return true;
}
...
}
and then I tried to make authorization via route group middleware:
Route::group(['middleware' => ['can:index,create,App\Project']], function () {
Route::resource('projects', 'ProjectController');
});
ofcourse I have created Project model and controller correctly, but calling index and create actions always returns 403 forbidden response. where is the problem?
Update:
removing one of actions from route middleware, results correct response. something like this:
Route::group(['middleware' => ['can:index,App\Project']], function () {
Route::resource('projects', 'ProjectController');
});
Looking through the docs the can middleware doesn't really lend itself to resources. You could use multiple middleware calls on the group but this would mean that your use would require all privileges to access the routes.
Your alternatives are:
Add $this->authorize(new App\Project) to your index and create methods in your controller. Laravel will use reflection to figure out what policy to use based on the method it is called from.
Or
In the __construct() method of your controller you could use:
$this->authorizeResource(App\Project::class);
This will require you to
create update, view and delete methods inside your Policy class. Each of these methods will be passed User $user, Project $project e.g.
public function view(User $user, Project $project)
{
return true;
}
FYI, if you leave off the method name with authorize() or you use authorizeResource() Laravel will map certain method names to different policy methods i.e. :
[
//ControllerMethod => PolicyMethod
'show' => 'view',
'create' => 'create',
'store' => 'create',
'edit' => 'update',
'update' => 'update',
'destroy' => 'delete',
];
You can override this by adding a resourceAbilityMap() method to your controller and returning a different array to the one above.
Hope this helps!
Spend hours on that ...
If your controller method does not take a Model as parameter, you also have to override the resourceMethodsWithoutModels().
protected function resourceAbilityMap()
{
Log::info("Inside resourceAbilityMap()");
return array_merge(parent::resourceAbilityMap(), [
//ControllerMethod => PolicyMethod
'index' => 'index',
'search' => 'search'
]);
}
protected function resourceMethodsWithoutModels(){
return array_merge(parent::resourceMethodsWithoutModels(), [
'search'
]);
}
Related
I created a custom authentication for my Laravel app following that tutorial: https://medium.com/#nasrulhazim/laravel-using-different-table-and-guard-for-login-bc426d067901
I adapted it to my needs but I didn't have to change much.
In the end, when I try to go the the /home route, but it says: "Route [login] not defined."
My guess is that a default behavior of the authentication call the login route instead of the /fidelite/login I've created.
Here is my provider:
fidelite' => [
'driver' => 'eloquent',
'model' => App\Fidelite::class,
],
And the guard
'fidelite' => [
'redirectTo' => 'fidelite.home',
'driver' => 'session',
'provider' => 'fidelite',
],
The routes defined in the web.php file
Route::prefix('fidelite')
->as('fidelite.')
->group(function() {
Route::get('/home', 'Home\FideliteHomeController#index')->name('home');
Route::namespace('Auth\Login')
->group(function() {
Route::get('login', 'FideliteController#showLoginForm')->name('login');
Route::post('login', 'FideliteController#login')->name('login');
Route::post('logout', 'FideliteController#logout')->name('logout');
Route::get('register', 'FideliteController#showRegisterForm')->name('register');
});
});
Basically, there is two controllers; the first one, FideliteController adds the middleware and show the needed forms to login / register
class FideliteController extends DefaultLoginController
{
protected $redirectTo = '/fidelite/home';
public function __construct()
{
$this->middleware('guest:fidelite')->except('logout');
}
public function showLoginForm()
{
return view('auth.login.fidelite');
}
public function showRegisterForm()
{
return view('auth.compte');
}
public function username()
{
return 'email';
}
protected function guard()
{
return Auth::guard('fidelite');
}
}
And the other one returns the /fidelite/home page when the user is logged
class FideliteHomeController extends Controller
{
public function __construct()
{
$this->middleware('auth:fidelite');
}
public function index()
{
return view('home.fidelite');
}
}
There is something I missing, but what ?
Many thanks for your help and time...
Found it ! Thanks to the Alpha whom helps me find the problem !
The problem was that the middleware I was using (Authenticate.php) was redirecting to the route('login') instead of the custom route I needed.
You are duplicating the login name route. change the name of login to any specific name that properly define your route behavior.
I am attempting to run middleware on specific routes as well as inside controller constructor.
However, it appears middleware that are defined inside the controller constructor are not being executed for the routes that include middleware.
Is this not possible? (all of these middleware are registered in kernel.php, all middleware in constructor working before adding middleware to route)
Route
Route::get('/{organization_slug}', function($organization_slug){
$organization = \App\Organization::where('slug', '=', $organization_slug)->first();
$app = app();
$controller = $app->make('\App\Http\Controllers\OrganizationController');
return $controller->callAction('details', $parameters = array($organization->id));
})->middleware('verifyorganizationslug');
Controller Constructor
public function __construct()
{
$this->middleware('auth', ['only' => ['create', 'update', 'store']]);
$this->middleware('accountactive', ['only' => ['create', 'update', 'store']]);
$this->middleware('ownsorganization', ['only' => ['update']]);
$this->middleware('verifyorganization', ['only' => ['details']]);
}
In gatherMiddleware, unique middleware are selected after merging route middleware with controller middleware.
public function gatherMiddleware()
{
if (! is_null($this->computedMiddleware)) {
return $this->computedMiddleware;
}
$this->computedMiddleware = [];
return $this->computedMiddleware = array_unique(array_merge(
$this->middleware(), $this->controllerMiddleware()
), SORT_REGULAR);
}
If you're viewing action mapped to details method, you should see verifyorganizationslug then verifyorganization applied.
Depending on which route you're viewing, the computedMiddleware will always have verifyorganizationslug middleware applied and other middleware specified for that route in the controller applied.
Route controller getMiddleware filters all middleware not belonging in that method.
public function getMiddleware($controller, $method)
{
if (! method_exists($controller, 'getMiddleware')) {
return [];
}
return collect($controller->getMiddleware())->reject(function ($data) use ($method) {
return static::methodExcludedByOptions($method, $data['options']);
})->pluck('middleware')->all();
}
IMPORTANT
Now, your code runs around the request-response Pipeline which ensures that middleware are applied in order explained above. You lose the application of controller middleware as isControllerAction returns false because it's a Closure and not a string.
Fast forward, you want to use:
Route::get('/{organization_slug}', '\App\Http\Controllers\FooController#details')
->middleware('foo');
Then resolve organization_slug inside of your controller.
public function details(Request $request, $organisation_slug)
{
$organization = \App\Organization::where('slug', '=', $organization_slug)
->first();
...
}
Or consider using Route binding to bind organization_slug route parameter to an instance of the organization.✌️
I've got the following routes:
Route::controllers([
'auth' => 'Auth\AuthController',
'password' => 'Auth\PasswordController',
'admin' => 'AdminController',
'site' => 'SiteController'
]);
And then I've got the following method in the SiteController:
/**
* Get site details and pass to view
*
* #param Site $site
* #return mixed
* #internal param $site_id
*/
public function getDetails( Site $site )
{
return $site;
}
When I go to the URL site.com/site/details/13 it doesn't return the site object.
I've added $router->model( 'one', 'App\Site' ); into the RouteServiceProvider and it works, but what if later down the road I want to add another controller like this but use it for jobs, and use the getDetails method again and pass through the App\Job object? It will automatically send the App\Site model instead.
So is there a way I can prevent this from happening?
My limited knowledge of Laravel tells me you can't have a model/object as parameter in your route controller functions, and that you don't need something like $router->model( 'one', 'App\Site' ); to do this.
I'm assuming you'd want to do something like this:
As for your routes:
Route::controllers([
'auth' => 'Auth\AuthController',
'password' => 'Auth\PasswordController',
'admin' => 'AdminController',
'site' => 'SiteController',
'jobs' => 'JobController',
]);
In your SiteController:
use Illuminate\Http\Request;
use App/Site; //replace with namespace of model
public function getDetails($id)
{
//code for fetching the site object, depends on how your structure is,
//like $site = App\Site::find($id); etc
return $site;
}
Similarly, your JobController will be something like:
use Illuminate\Http\Request;
use App/Job; //replace with namespace of model
public function getDetails($id)
{
//code for fetching the job object, depends on how your structure is,
//like $job = App\Job::find($id); etc
return $job;
}
Look here: Laravel Docs - Implicit Controllers
I'm trying to execute some code inside a Yii2 controller as I need some code from the model to be accessible within the behaviors section so I can pass the model as a parameter and avoid running duplicate queries; however I also need to be able to find out what action is being called, but I am not having much luck.
I have tried using beforeAction but it seems this gets run AFTER the behaviours code runs, so that doesn't help me.
I then tried using init, but it seems the action isn't available via $this->action->id at that point.
Some example code:
class MyController extends Controller {
public $defaultAction = 'view';
public function init() {
// $this->action not available in here
}
public function beforeAction() {
// This is of no use as this runs *after* the 'behaviors' method
}
public function behaviors() {
return [
'access' => [
'class' => NewAccessControl::className(),
'only' => ['view','example1','example2'],
'rules' => [
[
'allow' => false,
'authManager' => [
'model' => $this->model,
'other_param' => $foo,
'other_param' => $bar,
],
'actions' => ['view'],
],
// everything else is denied
],
],
];
}
public function viewAction() {
// This is how it is currently instantiated, but we want to instantiate *before* the behavior code is run so we don't need to instantiate it twice
// but to be able to do that we need to know the action so we can pass in the correct scenario
$model = new exampleModel(['scenario' => 'view']);
}
}
authManager is simply a reference to a member variable inside an extension of the AccessRule class.
Is there anyway I can do this?
Well, if I get you right, you are looking for something like this:
public function behaviors()
{
$model = MyModel::find()->someQuery();
$action = Yii::$app->controller->action->id;
return [
'someBehavior' => [
'class' => 'behavior/namespace/class',
'callback' => function() use ($model, $action) {
//some logic here
}
]
];
}
Because behaviors() is just a method, you can declare any variables and add any logic that you want in it, the only one convention that you must follow - is that return type must be an array.
If you use your custom behavior, you are able to use events() method where you can bind your behavior's methods to certain events. E.g.
class MyBehavior extends Behavior
{
public function events()
{
return [
\yii\web\User::EVENT_AFTER_LOGIN => 'myAfterLoginEvent',
];
}
public function myAfterLoginEvent($event)
{
//dealing with event
}
}
In this example myAfterLoginEvent will be executed after user successfully login into application. $event variable will be passed by framework and depending of event type it will contain different data. Read about event object
UPDATE:
As I can see now my answer was more generic about events and behaviors. And now when you added code, I can suggest to you to override behavior's beforeAction($action) method with the following code:
public function beforeAction($action)
{
$actionID = $action->id;
/* #var $rule AccessRule */
foreach ($this->rules as &$rule) {
$model = &$rule->authManager['model'];
//now set model scenario maybe like this
$model->scenario = $actionID;
}
//now call parent implementation
parent::beforeAction($action);
}
Also take a look at AccessControl implementation of beforeAction method, it invokes for each rule allows method with passing current action to it as a parameter. So if you have class that extends AccessRule, you can either override allows($action, $user, $request) method or matchCustom($action) method to set appropriate model scenario. Hope this will help.
One more alternative:
override controller's runAction($id, $params = []) method. Here $id is actionID - exactly what you need. Check id, set appropriate model scenario and call parent::runAction($id, $params);
I am recently started using laravel, i am using following restful approach, please suggest if this approach is good...
Author controller
class AuthorsController extends BaseController {
public $restful = true;
public function getIndex()
{
return View::make('authors.index')->with('title', 'Showing Authors')->with('authors', Author::all());
}
public function newAuthor()
{
if(Request::isMethod('post')){
$author = Author::create(array(
'name' => Input::get('name'),
'bio' => Input::get('bio')
));
if($author->id) return Redirect::route('authors')->with('message', 'The Author was created successfully!');
}
else{
return View::make('authors.new')->with('title', 'New Author');
}
}
}
Routes
Route::get('authors', array('as' => 'authors', 'uses' => 'AuthorsController#getIndex'));
Route::match(array('GET', 'POST'), 'authors/new', array('as' => 'new_author', 'uses' => 'AuthorsController#newAuthor'));
Please suggest if i am using correct approach for create method, and i am using same method for add form and post request.
thanks.
Your code probalby works, but you could improve it by joining your routes, controllers and migrations in one resource.
With laravel generator package
After installing that package, with the following command:
Route::resource("path","SomeController");
You will get generated resources for you app. That includes list of restful controller actions.
For example you for main GET method you will have generated index action in you controller.
Close. You shouldnt need to test the request method in a controller action if the routes/controller is set up properly.
Author Controller
class AuthorsController extends BaseController {
public function getIndex()
{
return View::make('authors.index')->with('title', 'Showing Authors')->with('authors', Author::all());
}
public function getNew()
{
return View::make('authors.new')->with('title', 'New Author'); //this view should contain a form that POST's to /authors/new
}
public function postNew() {
$author = Author::create(Input::all()); //note: use $fillable on your model here to prevent any extra fields from breaking things
return Redirect::action('AuthorsController#getNew')->with('message', 'The Author was created successfully!');
}
}
Routes
Route::controller("/authors", "AuthorsController")