How to use Authority package for Laravel - laravel

After some searching, I succesfully installed the Authority-l4 package to use for my Laravel project. The docs are clear but small (not much info/examples). This is what my config file looks like atm:
return array[
'initialize' => function($authority) {
$user = $authority->getCurrentUser();
$authority->addAlias('manage', ['create', 'read', 'update', 'delete']);
if($user->hasRole('admin')) {
//Admin can manage all resources
$authority->allow('manage', 'all');
}
// User can manage his own post
Authority::allow('manage', 'User', function($self, $user){
return $self->getCurrentUser()->id === $user->id;
});
// User can manage his own post
Authority::allow('manage', 'Post', function($self, $post){
return $self->getCurrentUser()->id === $post->id;
});
}
];
I have some questions about this:
How to add a role to a user? hasRole() exists, why not setRole()?
I noticed nothing gets saved into the database, isn't this better?
How do I use my database with Authority? Could someone give me a head start, I've been strugling four hours now.
In some articles they say that the class Role should be changed to have many permissions instead of a user having many permissions, isn't this better?
Probably I'm thinking way to difficult about this package, searching the internet doesn't help either. Any help is appreciated!

I'm the author of Authority, and I maintain Authority-l4 though it was written primarily by Conar Welsh.
Since roles, as defined in the package, are just an Eloquent relation so you can simply add them like any other relation in Eloquent.
I have no idea what you're asking here, can you rephrase?
Can you elaborate on the question beyond what's in the readme (the part just above General Usage)?
Probably - either works. You don't need to use this structure to use Authority-l4. It's just an optional structure that you are 100% free to setup as you'd like. I personally don't use this at all and just use the Authority facade that it generates. Most of my permissions aren't stored in the DB though so that plays a factor.
The idea behind Authority is that it is implementation agnostic. It genuinely does not care where you store your data, you just need to tell authority what to do with your rules. Reading the section of the readme referenced above and the readme on the Authority core repo should be able to give you a general idea of how it expects information to be loaded - anything beyond that is up to your discretion.

Related

How to feature test more complicated cases on Laravel using PHPUnit

I'm using Laravel for my project and I'm new to unit/feature testing so I was wondering what is the best way to approach more complicated feature cases when writing tests?
Let's take this test example:
// tests/Feature/UserConnectionsTest.php
public function testSucceedIfConnectAuthorised()
{
$connection = factory(Connection::class)->make([
'sender_id' => 1,
'receiver_id' => 2,
'accepted' => false,
'connection_id' => 5,
]);
$user = factory(User::class)->make([
'id' => 1,
]);
$response = $this->actingAs($user)->post(
'/app/connection-request/accept',
[
'accept' => true,
'request_id' => $connection->id,
]
);
$response->assertLocation('/')->assertStatus(200);
}
So we've got this situation where we have some connection system between two users. There is a Connection entry in the DB created by one of the users. Now to make it a successful connection the second user has to approve it. The problem is within the UserController accepting this through connectionRequest:
// app/Http/Controllers/Frontend/UserController.php
public function connectionRequest(Request $request)
{
// we check if the user isn't trying to accept the connection
// that he initiated himself
$connection = $this->repository->GetConnectionById($request->get('request_id'));
$receiver_id = $connection->receiver_id;
$current_user_id = auth()->user()->id;
if ($receiver_id !== $current_user_id) {
abort(403);
}
[...]
}
// app/Http/Repositories/Frontend/UserRepository.php
public function GetConnectionById($id)
{
return Connection::where('id', $id)->first();
}
So we've got this fake (factory created) connection in the test function and then we unfortunately are using its fake id to run a check within the real DB among real connections, which is not what we want :(
Researching I found an idea of creating interfaces so then we can provide a different method bodies depending if we're testing or not. Like here for GetConnectionById() making it easy to fake answers to for the testing case. And that seems OK, but:
for one it looks like a kind of overhead, besides writing tests I have to make the "real" code more complicated itself for the sole purpose of testing.
and second thing, I read all that Laravel documentation has to say about testing, and there is no one place where they mention using of interfaces, so that makes me wonder too if that's the only way and the best way of solving this problem.
I will try to help you, when someone start with testing it is not easy at all, specially if you don't have a strong framework (or even a framework at all).
So, let me try help you:
It is really important to differentiate Unit testing vs Feature testing. You are correctly using Feature test, because you want to test business logic instead of a class directly.
When you test, my personal recommendation is always create a second DB to only use with tests. It must be completely empty all the time.
So, for you to achieve this, you have to define the correct environment variables in phpunit.xml, so you don't have to do magic for this to work when you only run tests.
Also, use RefreshDatabase trait. So, each time you run a test, it is going to delete everything, migrate your tables again and run the test.
You should always create what you need to have as mandatory for your test to run. For example, if you are testing if a user can cancel an order he/she created, you only need to have a product, a user and an invoice associated with the product and user. You do not need to have notifications created or anything not related to this. You must have what you expect to have in the real case scenario, but nothing extra, so you can truly test that it fully works with the minimum stuff.
You can run seeders if your setup is "big", so you should be using setup method.
Remember to NEVER mock core code, like request or controllers or anything similar. If you are mocking any of these, you are doing something wrong. (You will learn this with experience, once you truly know how to test).
When you write tests names, remember to never use if and must and similar wording, instead use when and should. For example, your test testSucceedIfConnectAuthorised should be named testShouldSucceedWhenConnectAuthorised.
This tip is super personal: do not use RepositoryPattern in Laravel, it is an anti-pattern. It is not the worst thing to use, but I recommend having a Service class (do not confuse with a Service Provider, the class I mean is a normal class, it is still called Service) to achieve what you want. But still, you can google about this and Laravel and you will see everyone discourages this pattern in Laravel.
One last tip, Connection::where('id', $id)->first() is exactly the same as Connection::find($id).
I forgot to add that, you should always hardcode your URLs (like you did in your test) because if you rely on route('url.name') and the name matches but the real URL is /api/asdasdasd, you will never test that the URL is the one you want. So congrats there ! A lot of people do not do this and that is wrong.
So, to help you in your case, I will assume you have a clear database (database without tables, RefreshDatabase trait will handle this for you).
I would have your first test as this:
public function testShouldSucceedWhenConnectAuthorised()
{
/**
* I have no idea how your relations are, but I hope
* you get the main idea with this. Just create what
* you should expect to have when you have this
* test case
*/
$connection = factory(Connection::class)->create([
'sender_id' => factory(Sender::class)->create()->id,
'receiver_id' => factory(Reciever::class)->create()->id,
'accepted' => false,
'connection_id' => factory(Connection::class)->create()->id,
]);
$response = $this->actingAs(factory(User::class)->create())
->post(
'/app/connection-request/accept',
[
'accept' => true,
'request_id' => $connection->id
]
);
$response->assertLocation('/')
->assertOk();
}
Then, you should not change anything except phpunit.xml environment variables pointing to your testing DB (locally) and it should work without you changing anything in your code.

Route and view naming conventions

I am looking for some input regarding the naming conventions I use for route names and view directory structures.
Say I have the following routes:
Route::get('/teams/choose', 'ChooseTeamController#index')->name('teams.choose.index');
Route::post('/teams/choose', 'ChooseTeamController#choose')->name('teams.choose');
Route::get('/teams/{team}/manage', 'ManageTeamController#index')->name('teams.team.manage.index');
For the get routes, I would nornally put the views in a directory structure matching the route name. E.g. resources/views/teams/team/manage/index.blade.php. However, I feel that this is way too verbose.
I feel that it would be confusing all round (to myself and other developers) if I was to use a view directory structure like so, rather than the last example: resources/views/team/manage/index.blade.php- the plural of team is not used, so when I have other views, like so (using the original examples convention): resources/views/teams/choose.index they dont visually have the relationship intended. I.e. they have a differing 'root' directory- teams vs team.
Any input or advice would be appreciated.
For the get routes, I would normally put the views in a directory structure matching the route name. E.g. resources/views/teams/team/manage/index.blade.php. However, I feel that this is way too verbose.
I agree.
From the Laravel docs:
Laravel uses the typical RESTful "CRUD" approach when assigning resource routes to a controller. Each verb (i.e. GET, POST, PUT, DELETE) gets a designated URI, an action (technically, a controller method) and a route-name (sometimes, /path/to/blade/view).
So, from your snippet:
// return view(teams.index)
Route::get('/teams', 'TeamController#index');
// return view(teams.create)
Route::get('/teams/create', 'TeamsController#create');
// redirect('/home');
Route::post('/teams', 'TeamController#store');
// return view('teams.profile')
Route::get('/teams/profile', 'TeamController#profile')->name('profile');
I use this resource table to remind me what-to-do and what-not-do all the time.
Perhaps, inspecting some of the awesome Laravel codebases, might help. Plus, a perspective on how other teams are doing things is always priceless.
I found these to be very helpful:
RESTful API Best Practices and Common Pitfalls
RESTful API Design - A Cookbook
Update
The key is to stick to the standard CRUD actions i.e. index, show, create, store, edit, update and delete. The views will fall, right into their place.
Check out Adam Wathan's talk at Laracon EU as he demonstrates how, anything can be CRUDDY with a little imagination.
There are so many ways to maintain routes based on the requirement but i always follow below guidelines which helps me to maintain the file structure and easy to understand.
//listing
Route::get('/teams', 'TeamController#index');
//Create
Route::get('/teams/create', 'TeamController#create');
//Store
Route::post('/teams/store', 'TeamController#store');
//Show
Route::get('/teams/{id}', 'TeamController#show');
//Edit
Route::get('/teams/{id}/edit', 'TeamController#edit');
//Update
Route::put('/teams/{id}/update', 'TeamController#update');
//Delete
Route::delete('/teams/{id}/delete', 'TeamController#delete');
for more information related to proper naming convention you may follow below link
https://laravel.com/docs/7.x/controllers#restful-nested-resources
if you are building with consuming by api as a concern, you wouldn't need the create and edit forms so endpoints can be reduced to:
//listing
Route::get('/teams', 'TeamController#index');
//Store
Route::post('/teams', 'TeamController#store');
//Show
Route::get('/teams/{id}', 'TeamController#show');
//Update
Route::put('/teams/{id}', 'TeamController#update');
//Delete
Route::delete('/teams/{id}', 'TeamController#delete');

Creating an admin permissions section in laravel

I've created a couple of pages they are users, groups and permissions.
I would like the admin to be able to create groups and set what those groups can do via the permissions page.
So on the permissions page I would have a list of things a user could do e.g.: add content, delete content.
And if I check the add content box then the group can only add content and not delete content.
The problem I'm having is that I don't know where to go to look for information on how to go about it. I've already got my database set up and I'm thinking maybe sessions and routes is the way to go, but I'm not sure.
Frameworks is the way to go for something this complex. I'm working on a very similar project for my work (a dashboard to do different things based on User Role/Permissions) and I found it incredibly difficult to manage without the use of a framework. I would highly recommend Cartalyst/Sentry for this. It turns complex database operations like checking permissions, update permissions, creating groups etc into simple one. Here is a link to the manual:
Cartalyst/Sentry
It has a database backend already created (and modifiable) for you, so you simply follow the installation instructions and go over the documentation to get a better understanding. In your example, it would be as simple as creating a group and it's permissions:
// Define permissions (1 is allowed, 0 is not allowed)
$permissions = array('content.create' => 1, 'content.delete' => 1, etc etc...));
$group = Sentry::createGroup(array('name' => 'Admin', 'permissions' => $permissions));
Creating a user and adding them to the group:
$user = Sentry::createUser(array('email' => 'test#test.com', 'first_name' => ..., etc));
$group = Sentry::findGroupByName('Admin');
$user->addGroup($group);
And then checking their permissions during routing:
$user = Sentry::check(); // Aka get the current user.
if($user->hasAccess('content.create'){
// Continue
} else {
// Redirect to error page, etc
}
Now that's a brief overview of the system, and I assume you know how to use controllers and routes, but play around with it and I'm sure you'll come to see how powerful this Framework is when working with Laravel.
Hope that helps!
Then checking whether or not a user

Sentry & Laravel, getting users within a group. changing findAllUsersWithAccess to have pagination

I'm trying to find all users w/ a specific permissions list in Sentry with laravel. The problem is that Sentry::findAllUsersWithAccess() returns an array().
as stated in their github repository i pinpointed their code to be
public function findAllWithAccess($permissions)
{
return array_filter($this->findAll(), function($user) use ($permissions)
{
return $user->hasAccess($permissions);
});
}
right now, it gets all users and filter it out with users with permission list. the big problem would be when I as a developer would get the set of users, it'll show ALL users, i'm developing an app which may hold thousands of users and i only need to get users with sepcific permission lists.
With regards to that would love to use one with a ->paginate() capability.
Any thoughts how to get it without getting all the users.
Why dont you override the findAllWithAccess() method and write your own implementation, which uses mysql where instead of array_filter().
I dont know your project structure and the underlying db schema, so all i can give you atm is the link to the eloquent documentation Querying Relations (whereHas).
In case you dont know where to start: its always a good idea to look at the ServiceProvider (SentryServiceProvider, where the UserProvider, which holds the findAllWidthAccess() method, is registered). Override the registerUserProvider method and return your own implementation of the UserProvider (with the edited findAllWithAccess() method).
Hope that will point you in the right direction.
In Laravel you can do pagination manually on arrays:
$paginator = Paginator::make($items, $totalItems, $perPage);
Check the docs: http://laravel.com/docs/pagination

Basset 4 (pre-beta)

I'm using Basset 4 to manage assets.
In the config file i'm declaring a collection 'admin'
return array(
'collections' => array(
'admin' => function($collection)
{
$collection->directory('assets/js', function($collection)
{
$collection->add('vendor/jquery-1.9.1.min.js');
});
},
),
...
)
later in a view, I would like to add an extra file in admin collection.
I've try the following code, but it doesn't work:
Basset::collection('admin', function($collection)
{
$collection->add('function.js');
});
Is there a way to add file into a collection from a view or from a controller?
Thank you
Basset isn't really designed to work like that. You should be defining all your assets within that initial call, even though the ability to add assets throughout the execution of an application is possible, it's not recommended.
When building a collection assets that are added for a particular route won't be available to the builder since Artisan doesn't fire any routes, etc.
Adjusting a collection in numerous places can often lead to confusion further down the line.
I know this isn't ideal as you're probably looking at implementing page specific JavaScript, correct? I've thought about it but can't really think of a clean solution (suggestions?), although I've heard of people assigning a unique ID to the body or perhaps some classes that their JavaScript can then attach themselves to.
It's not brilliant but that's the best I can give you at the moment.

Resources