How does one load gate defines when testing a Laravel application? - php

I'm writing tests for a Laravel application. In my AuthServiceProvider->boot(), I define a number of user abilities with $gate->define() based on a permissions table in my database.
Basically this:
foreach ($this->getPermissions() as $permission) {
$gate->define($permission->name, function ($user) use ($permission) {
return $user->hasPermission($permission->name);
});
}
In my tests I'm creating permissions on the fly, but the AuthServiceProvider has already booted up, which means I can't verify user permissions with #can, Gate, etc.
Is there a proper way to deal with this issue?

I know I'm a bit late for the party on this one, but still - I just had the same problem myself and hence this question doesn't have a comprehensive answer, here is my solution for the same issue (in Laravel 5.3):
I've got this in my app\Providers\AuthServiceProvider:
/**
* Register any authentication / authorization services.
*
* #param Gate $gate
*/
public function boot(Gate $gate)
{
$this->registerPolicies();
if (!app()->runningInConsole()) {
$this->definePermissions($gate);
}
}
/**
* #param Gate $gate
*/
private function definePermissions(Gate $gate)
{
$permissions = Permission::with('roles')->get();
foreach($permissions as $permission) {
$gate->define($permission->key, function($user) use ($permission) {
return $user->hasRole($permission->roles);
});
}
}
This takes care of the normal application flow when not testing and disables the premature policy registration when testing.
In my tests/TestCase.php file I have the following methods defined (note that Gate points to Illuminate\Contracts\Auth\Access\Gate):
/**
* Logs a user in with specified permission(s).
*
* #param $permissions
* #return mixed|null
*/
public function loginWithPermission($permissions)
{
$user = $this->userWithPermissions($permissions);
$this->definePermissions();
$this->actingAs($user);
return $user;
}
/**
* Create user with permissions.
*
* #param $permissions
* #param null $user
* #return mixed|null
*/
private function userWithPermissions($permissions, $user = null)
{
if(is_string($permissions)) {
$permission = factory(Permission::class)->create(['key'=>$permissions, 'label'=>ucwords(str_replace('_', ' ', $permissions))]);
if (!$user) {
$role = factory(Role::class)->create(['key'=>'role', 'label'=>'Site Role']);
$user = factory(User::class)->create();
$user->assignRole($role);
} else {
$role = $user->roles->first();
}
$role->givePermissionTo($permission);
} else {
foreach($permissions as $permission) {
$user = $this->userWithPermissions($permission, $user);
}
}
return $user;
}
/**
* Registers defined permissions.
*/
private function definePermissions()
{
$gate = $this->app->make(Gate::class);
$permissions = Permission::with('roles')->get();
foreach($permissions as $permission) {
$gate->define($permission->key, function($user) use ($permission) {
return $user->hasRole($permission->roles);
});
}
}
This enables me to use this in tests in multiple ways. Consider the use cases in my tests/integration/PermissionsTest.php file:
/** #test */
public function resource_is_only_visible_for_those_with_view_permission()
{
$this->loginWithPermission('view_users');
$this->visit(route('dashboard'))->seeLink('Users', route('users.index'));
$this->visit(route('users.index'))->assertResponseOk();
$this->actingAs(factory(User::class)->create());
$this->visit(route('dashboard'))->dontSeeLink('Users', route('users.index'));
$this->get(route('users.index'))->assertResponseStatus(403);
}
/** #test */
public function resource_action_is_only_visible_for_those_with_relevant_permissions()
{
$this->loginWithPermission(['view_users', 'edit_users']);
$this->visit(route('users.index'))->seeLink('Edit', route('users.edit', User::first()->id));
$this->loginWithPermission('view_users');
$this->visit(route('users.index'))->dontSeeLink('Edit', route('users.edit', User::first()->id));
}
This works just fine in all my tests. I hope it helps.

public function boot(GateContract $gate)
{
parent::registerPolicies($gate);
$gate->before(function($user, $ability) use ($gate){
return $user->hasPermission($ability);
});
}
I haven't extensively tested this, but it seems to work from my quick tests.

I'm not sure what the "proper" way (if there is one) to define a gate for testing. I couldn't find an answer for this after looking at the documentation and searching, but this seems to work in a pinch in Laravel 5.7:
Defining a gate in a model factory state:
$factory->state(App\User::class, 'employee', function () {
Gate::define('employee', function ($user) {
return true;
});
return [];
});
This test function will have both the 'employee' and the 'admin' gate applied since we are using the 'employee' state when creating the user:
/** #test */
public function an_admin_user_can_view_the_admin_page()
{
$user = factory('App\User')->state('employee')->make();
$this->actingAs($user);
Gate::define('admin', function ($user) {
return true;
});
$this->get('/admin')
->assertOk();
}
I know this is a really old question, but it was the top result in a search and hopefully can help someone.
Don't forget to use the Gate facade:
use Illuminate\Support\Facades\Gate;

You could do something like this inside AuthServiceProvider
First import the necessary packages
use Illuminate\Auth\Access\Gate;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
and then add this boot() method
public function boot(GateContract $gate)
{
parent::registerPolicies($gate);
$gate->define('update-post', function ($user, $post, $isModerator) {
// check if user id equals post user id or whatever
if ($user->id === $post->user->id) {
return true;
}
// you can define multiple ifs
if ($user->id === $category->user_id) {
return true;
}
if ($isModerator) {
return true;
}
return false;
});
// you can also define multiple gates
$gate->define('update-sub', function($user, $subreddit) {
if($user->id === $subreddit->user->id) {
return true;
}
return false;
});
And then in your controller you could do something like this
if (Gate::denies('update-post', [$post, $isModerator])) {
// do something
}

Related

how can Prevent access another page with 3 type users in Laravel

i have problem with laravel cus im begginer but i work with php languge very well
and my Question:
I created a table for users in my database and create column for type
There are 3 user types in my table:
customers - Workers - Factories
How can i use middlewarre or anything else Prevent access to other pages
public function Signupuser(Request $request){
$email=$request['email'];
$username=$request['username'];
$tell=$request['mobilenumber'];
$pass=bcrypt($request['password']);
$status_reg=$request['status_register'];
$usertable=new UserTable();
$usertable->username=$username;
$usertable->email=$email;
$usertable->Password=$pass;
$usertable->Tell=$tell;
$usertable->StatusReg=$status_reg;
$usertable->save();
Auth::login($usertable);
if($status_reg=='factory'){
return redirect()->route('FactoryDashboard');
}
if($status_reg=='worker'){
return redirect()->route('WorkerDashboard');
}
if($status_reg=='customer'){
return redirect()->route('CustomerDashboard');
}
}
public function signinuser(Request $request){
$email=$request['email'];
$pass=$request['pass'];
if (Auth::attempt(['email'=>$email,'password'=>$pass])){
$status = Auth::user()->StatusReg;
return $status;
}
else{
return "nokey";
}
}
i used with one middleware but this middleware dosent work
<?php
namespace App\Http\Middleware;
use App\UserTable;
use Closure;
class WorkerMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if($request->user() && $request->user()->StatusReg !='worker'){
return redirect('homepage');
}
return $next($request);
}
}
please help guys
Your scenario is usually dealt with by using the Authorization service of Laravel.
For example, you could add the following to your app\Providers\AuthServiceProvider.php file:
Gate::define('factory', function ($user) {
return $user->StatusReg == 'factory';
});
Gate::define('worker', function ($user) {
return $user->StatusReg == 'worker';
});
Gate::define('customer', function ($user) {
return $user->StatusReg == 'customer';
});
And then you can use it in your application like the following:
if (Gate::allows('worker')) {
//...
}
if (Gate::denies('customer')) {
//...
}
There are plenty more usage examples in the docs:
https://laravel.com/docs/5.6/authorization

Multiple AdminMiddleware Ambiguity

I have multiple admin system: one is super admin and the other is normal admin, distinguished by is_admin attribute in users table.
And these two middlewares:
SuperAdminMiddleware.php
public function handle($request, Closure $next, $guard = null)
{
if(Auth::check())
{
if($request->user()->is_admin==1)
{
return $next($request);
}
return redirect('/login');
}
else
{
return redirect('/login');
}
}
and, NormalAdminMiddleware.php
public function handle($request, Closure $next, $guard = null)
{
if(Auth::check())
{
if($request->user()->is_admin==2)
{
return $next($request);
}
return redirect('/login');
}
else
{
return redirect('/login');
}
}
and in loginController:
protected function authenticated()
{
if (auth()->user()->is_admin==1) {
return redirect('/super-admin');
}
else if(auth()->user()->is_admin==2){
return redirect('/normal-admin');
}
else {
return redirect('/home');
}
}
Now, Delete and Read should be designed in such a way that super admin can delete and see all users details, while normal admin can only see their city's user.
id name city is_admin
1 Non Maety 1
3 Pom Lorey 2
4 Rom Lorey 0
2 Yer Easter 0
Non should be able to see all. while Pom should see only id 3 and 4.
If i put show and delete routes under SuperAdminMiddleware, Normal Admin couldnot see their city's records.
Route::group(['middleware' => ['App\Http\Middleware\SuperAdminMiddleware']], function () {
Route::get('/show/{id}', 'MyController#show');
Route::post('/delete', 'MyController#delete');
});
And if i put these routes under both SuperAdminMiddleware and NormalAddminMiddleware. NormalAdminMiddleware can also see other city's records.
Route::group(['middleware' => ['App\Http\Middleware\NormalAdminMiddleware']], function () {
Route::get('/show/{id}', 'MyController#show');
Route::post('/delete', 'MyController#delete');
});
How do i overcome this situation?
You can solve it with a policy:
class UserPolicy
{
/**
* Determine if the given user can be viewed by the user.
*
* #param \App\User $user
* #param \App\User $account
* #return bool
*/
public function view(User $user, User $account)
{
switch($user->is_admin) {
case 1:
return true;
case 2:
return $user->city == $account->city;
default:
return 0;
}
}
/**
* Determine if the given user can be updated by the user.
*
* #param \App\User $user
* #param \App\User $account
* #return bool
*/
public function update(User $user, User $account)
{
switch($user->is_admin) {
case 1:
return true;
case 2:
return $user->city == $account->city;
default:
return 0;
}
}
}
User would be the authenticated user model, account would be the user model that should be viewed.
After you registered your policy (https://laravel.com/docs/5.4/authorization#registering-policies) you can call it in the function of your controller like:
public function show(User $user) {
$this->can('view', $user);
}
I don't understand your purpose clearly by reading the question. If you put your methods under SuperAdminMiddleware, normal admin should be rejected and redirect to '/login'. If you want to control the operation of admin, I think middleware can't solve the problem. As the previous answer, when operate the data of database, judge whether he is superAdmin or not.

Laravel - Implement eloquent relations in repository

I have 3 models for managing user permissions.
class User extends Authenticatable
{
public function roles()
{
return $this->belongsToMany('App\Models\Role');
}
}
class Role extends Model
{
public function users()
{
return $this->belongsToMany('App\Models\User');
}
public function permissions()
{
return $this->belongsToMany('App\Models\Permission');
}
}
class Permission extends Model
{
public function roles()
{
return $this->belongsToMany('App\Models\Role');
}
public function roleHavePermission(Role $role)
{
if ($this->roles()->find($role->id)) {
return true;
}
return false;
}
public function userHavePermission(User $user = null)
{
$roles = [];
if (is_null($user)) {
$roles[] = Role::where('slug', 'guest')->first();
} else {
foreach ($user->roles as $role) {
$roles[] = $role;
}
}
foreach ($roles as $role) {
if ($this->roleHavePermission($role)) {
return true;
}
}
return false;
}
}
Now because my application is grown, I'm moving to repositories. For example this is my PermissionRepository:
class PermissionRepository implements PermissionRepositoryInterface
{
protected $roleRepository;
/**
* PermissionRepository constructor.
* #param RoleRepositoryInterface $roleRepository
*/
public function __construct(RoleRepositoryInterface $roleRepository)
{
$this->roleRepository = $roleRepository;
}
public function action($routeName)
{
return Permission::where('action', $routeName)->first();
}
}
How can I implement roleHavePermission and userHavePermission in this repository? I tried implementing roles method with this syntax:
public function roles()
{
return Permission::roles();
}
But it wont work since Permission's roles method can not called statically. Thanks.
In fact in my opinion you shouldn't implement it in repository. When you call
$permission = $permissionRepository->action('abc.name');
you are getting permission object, so there is no point to implement it in repository. All you need is running:
$roles = $permission->roles;
In theory you could add for example into PermissionRepository method like this:
public function getRoles(Permission $permission)
{
return $permission->roles;
}
so you could now run
$roles = $permissionRepository->getRoles($permission);
to get roles of given permission but I don't see any point to do it like this.

Slim 3 Implement Route Filtering

I am currently trying to implement route authentication filtering in Slim 3. What I would like to do is:
$app->get("/route", Filter::$guest(), function ($request, $response, $args) {
...
});
or maybe
$app->get("/route", function ($resquest, $response, $args) {
})->add(Filter::Admin);
and the Filter class would be:
class Filter
{
public static admin()
{
// Check if user is an admin.
// If not, throw an Error
}
...
In Slim 2, I could use someting like this
Filter.php
$authenticationCheck = function ($required) use ($app) {
return function () use ($required, $app) {
if ((!$app->auth && $required) || ($app->auth && !$required)) {
$app->redirect($app->urlFor("home"));
}
};
};
$authenticated = function () use ($authenticationCheck) {
return $authenticationCheck(true);
};
$guest = function () use ($authenticationCheck) {
return $authenticationCheck(false);
};
$admin = function () use ($app) {
return function () use ($app) {
if (!$app->auth || !$app->auth->isAdmin()) {
$app->notFound();
}
};
};
and in routes I could do:
$app->get("/route", $guest(), function () use ($app) {
//Route
});
I know that I can get the route through middleware, but I can't think of a good way to diffrenciate between a "admin" route and a normal route without having to build some sort of list.
You could create a basic middleware class Authorization:
<?php
class Authorization
{
/**
* Authorization middleware invokable class
*
* #param \Psr\Http\Message\ServerRequestInterface $request PSR7 request
* #param \Psr\Http\Message\ResponseInterface $response PSR7 response
* #param callable $next Next middleware
*
* #return \Psr\Http\Message\ResponseInterface
*/
public function __invoke($request, $response, $next)
{
$user = ""; //It should come from some place :)
if(!$this->isAuthorized($user)){
return $response->withRedirect('/notAuthorized');
}
return $next($request, $response);
}
/**
* Check if the given user is authorized.
*
* #param string $user The user to check.
*
* #return boolean True if the user is authorized, false otherwise.
*/
protected function isAuthorized($user){
return false;
}
}
Then you can extend it and create one middleware for guest authorization and another one for admin authorization:
<?php
class GuestAuthorization extends Authorization
{
protected function isAuthorized($user){
//Are you a guest?
$isGuest = true; //Your magic business here
return $isGuest;
}
}
class AdminAuthorization extends Authorization
{
protected function isAuthorized($user){
//Are you an admin?
$isAdmin = false; //Your magic business here
return $isAdmin;
}
}
Let's try with some routes and define the notAuthorized one:
<?php
$app->get("/guestRoute", function ($resquest, $response, $args) {
return $response->write("You're a guest");
})->add(new \GuestAuthorization());
$app->get("/adminRoute", function ($resquest, $response, $args) {
return $response->write("You're an admin");
})->add(new \AdminAuthorization());
$app->get("/notAuthorized", function ($resquest, $response, $args) {
return $response->write("You're not authorized for this, my son!");
});
PROs:
you can handle in different ways the authorization for every role;
you can add multiple middlewares for a single route.
CONs:
you can't handle in this way dynamic roles;
one middleware for each role.

Custom authentication issue in Yii2

I have added a custom authentication component for a Yii2 RESTful project and it is validating credentials OK but it is not returning the valid User object to \Yii::$app->user
The component looks like this:
public function authenticate($user, $request, $response) {
$bearerToken = \Yii::$app->getRequest()->getQueryParam('bearer_token');
$user = Account::findIdentityByAccessToken($bearerToken);
return $user;
}
And the Account model method looks like this:
public static function findIdentityByAccessToken($token, $userType = null) {
return static::findOne(['bearer_token' => $token]);
}
I can see $user is the expected record of Account when debugging in the authenticate() method but \Yii::app()->user seems to be a newly instatiated user. \Yii::app()->user->identity is equal to null.
Can anyone see what I'm doing wrong here?
To login user this is not enough:
Account::findIdentityByAccessToken($bearerToken);
You need to call $user->login($identity) inside authentificate(). See for example how it's implemented in yii\web\User loginByAccessToken():
public function loginByAccessToken($token, $type = null)
{
/* #var $class IdentityInterface */
$class = $this->identityClass;
$identity = $class::findIdentityByAccessToken($token, $type);
if ($identity && $this->login($identity)) {
return $identity;
} else {
return null;
}
}
So you can also call it in your custom auth method:
$identity = $user->loginByAccessToken($accessToken, get_class($this));
See for example how it's implemented in yii\filters\auth\QueryParamAuth.
And you also need to return $identity, not $user. Also handling failure is missing in your code. See how it's implemented in built-in auth methods:
HttpBasicAuth
HttpBearerAuth
QueryParamAuth
More from official docs:
yii\web\User login()
yii\filters\auth\AuthInterface
Update:
Nothing forces you to use loginByAccessToken(), I just mentioned it as an example.
Here is an example of custom auth method that I wrote quite a while ago, not sure if it's 100% safe and true, but I hope it can help you to understand these details:
Custom auth method:
<?php
namespace api\components;
use yii\filters\auth\AuthMethod;
class HttpPostAuth extends AuthMethod
{
/**
* #see yii\filters\auth\HttpBasicAuth
*/
public $auth;
/**
* #inheritdoc
*/
public function authenticate($user, $request, $response)
{
$username = $request->post('username');
$password = $request->post('password');
if ($username !== null && $password !== null) {
$identity = call_user_func($this->auth, $username, $password);
if ($identity !== null) {
$user->switchIdentity($identity);
} else {
$this->handleFailure($response);
}
return $identity;
}
return null;
}
}
Usage in REST controller:
/**
* #inheritdoc
*/
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpPostAuth::className(),
'auth' => function ($username, $password) {
$user = new User;
$user->domain_name = $username;
// This will validate password according with LDAP
if (!$user->validatePassword($password)) {
return null;
}
return User::find()->username($username)->one();
},
];
return $behaviors;
}
Specifying $auth callable is also can be found in HttpBasicAuth.

Categories