ella-stinnes liked a comment+100 XP
2mos ago
You don’t need a ProjectUser model just to authorize “assigning a project to a user”.
In Laravel, the “pivot” is usually not treated as a first-class resource for authorization; instead you authorize the action on one of the real models (most commonly User or Project) and pass the other model(s) as arguments.
- Define a custom ability on UserPolicy
// app/Policies/UserPolicy.php
public function assignProject(User $actor, User $target, Project $project): bool
{
// 1) Full Admin: can link users to projects within the user's company
if ($actor->isFullAdmin()) {
return $target->company_id === $project->company_id;
}
// 2) Company Admin: only within their own company
if ($actor->isCompanyAdmin()) {
return $actor->company_id === $target->company_id
&& $actor->company_id === $project->company_id;
}
return false;
}
- Use it in controller (best place, because you have all objects)
public function update(User $user, Request $request)
{
$project = Project::findOrFail($request->project_id);
$this->authorize('assignProject', [$user, $project]);
$user->projects()->syncWithoutDetaching([$project->id]);
}
Route middleware: possible, but you’ll still need the objects
Route ->can() works well when your policy method can be resolved from route params. Since the ability needs User + Project, your route must provide both:
Route::post('/users/{user}/projects/{project}', [UserProjectController::class, 'store'])
->can('assignProject', ['user', 'project']);
This calls UserPolicy@assignProject(auth()->user(), $user, $project).
If your “edit” page doesn’t include {project} in the URL, then ->can() can only authorize a broader ability like “canManageUserProjects” (no specific project yet).
ella-stinnes wrote a reply+100 XP
2mos ago
My solution of a policy for a pivot table wasn't mentioned in the docs, so your explanation was what I was looking for and makes sense - thank you!
ella-stinnes wrote a reply+100 XP
2mos ago
-
Full Admin - Can link any user to projects that are associated with the users' company.
-
Company Admin - Can only view users associated with their own company. They can therefore only link these users to projects associated with their company.
ella-stinnes started a new conversation+100 XP
2mos ago
- projects
- project_user
- users
I want to create an authorization policy for the 'project_user' pivot table to define who is able to assign projects to a given user.
At the moment, I don't have a ProjectUser model class. Will I need to create this for the ProjectUserPolicy?
ella-stinnes wrote a reply+100 XP
3mos ago
To check my understanding, I would define a gate for the page access as follows:
Gate::define('companies-index', function (User $user) {
return $user->isRole([UserRole::Administrator, UserRole::CompanyAdministrator]);
});
Then the policy would determine whether they can viewAny and/or view a specific model as follows?
A company administrator would not be able to viewany so a query scope would be applied to the index page query?
public function viewAny(User $user): bool
{
return $user->isRole([UserRole::Administrator]);
}
public function view(User $user, Company $company): bool
{
if ($user->isRole([UserRole::Administrator]))
return true;
if($user->isRole([UserRole::CompanyAdministrator])) {
return $user->company_id === $company->id;
return false;
}
ella-stinnes started a new conversation+100 XP
3mos ago
Both an administrator and company administrator should be able to access the companies.index page.
-
Administrator - should be able to list all companies.
-
Company Administrator - should only list the companies they are related to.
I've seen differing opinions on the policy viewAny method.
Should I be using this to define whether the user role can access the index page as I have below:
public function viewAny(User $user): bool
{
return $user->isRole([UserRole::Administrator, UserRole::CompanyAdministrator]);
}
viewAny implies the CompanyAdministrator role can view any company.
Should I therefore be creating another method for allowing access to the index page (if so, what would you name this)?
ella-stinnes liked a comment+100 XP
3mos ago
@ella-stinnes Sounds like data providers and/or the “test with” utility would be a good solution for your use cases. They basically let you re-run a single test case multiple times, but with a different input each time.
The application has 8 different user roles, some users can access the full CRUD, some are read only and others cannot see the company details at all.
Should I be creating 8 tests for the different user roles?
Using a data provider, you’d have a single test case (e.g. test_create_company), that could take a user role and the expected result as inputs:
#[TestWith(['admin', true])]
#[TestWith(['moderator', true])]
#[TestWith(['member', false])]
// And your other roles...
public function test_create_company(string $role, bool $expected): void
{
$user = User::factory()->role($role)->createOne();
$this->assertDatabaseEmpty('companies');
$response = $this->actingAs($user)->post('/companies', [
// Valid company data...
]);
if ($expected) {
$response->assertRedirect()->assertValid();
$this->assertDatabaseHas('companies', [
// Fields and values that should exist in database...
]);
} else {
$response->assertForbidden();
$this->assertDatabaseEmpty('companies');
}
}
So the above test case will create a user with the given role and attempt to create a company. Then, depending on the expected result, will either assert the request was successful and a record was inserted into the database, or assert a 403 Forbidden response was returned and no company records were created.
Once I've tested the authorisation, I'm guessing I should have form validation tests - if there's 10 fields on the page, do I create 10 validation tests? Or one test to cover all 10 fields?
Again, you can use a data provider for say, testing required fields. For most resources in my applications, I‘ll have a test method that looks like this:
#[TestWith(['name'])]
#[TestWith(['description'])]
#[TestWith(['email'])]
#[TestWith(['telephone_number'])]
#[TestWith(['website_url'])]
public function test_field_is_required(string $field): void
{
$user = User::factory()->role('admin')->createOne();
$data = [
'name' => fake()->company(),
'email' => fake()->safeEmail(),
// All other fields with a valid value...
];
Arr::forget($data, $field);
$this
->actingAs($user)
->post('/companies', $data)
->assertInvalid($field);
}
The above test case will then be re-ran multiple times with a different field name provided as an argument each time. The test case has an array of “valid” data, but then removes the named field, and asserts the request throws a validation error for that named field.
ella-stinnes wrote a reply+100 XP
3mos ago
Thank you both, I'll look at the Pest Driven Laravel series although this appears to be TDD?
Does this cover a general rule of thumb as to what should be tested?
ella-stinnes started a new conversation+100 XP
3mos ago
I'm new to testing so I'm trying to figure out what I should test.
I have a CRUD for managing company details.
The application has 8 different user roles, some users can access the full CRUD, some are read only and others cannot see the company details at all.
Should I be creating 8 tests for the different user roles?
Once I've tested the authorisation, I'm guessing I should have form validation tests - if there's 10 fields on the page, do I create 10 validation tests? Or one test to cover all 10 fields?
Then there's the store/update methods, I'm guessing I assert the database contains the values.
Do you have a testing workflow for a standard CRUD as to what should be covered?
I'm not sure if I'm writing too many tests.