Authorization
Overview
Authorization is what ensures an entity has the ability to perform a given task. Laravel provides gates and policies — they are simple but powerful ways to answer this problem.
Authorization needs to be performed on the server. This is usually done through User#can
or Gate::authorize
. Unfortunately, this is not accessible when working in single-file components.
Hybridly solves this issue by providing a decorator around data objects. This decorator makes policies' actions typeable, so they can be used in single-file component by the can
function.
Using data resources
First, a data object extending Hybridly\Support\Data\DataResource
needs to be created. This class exposes an $authorizations
array which should contain the names of the actions that need to be exposed.
<?php
namespace App\Data;
use Carbon\Carbon;
use Hybridly\Support\Data\DataResource;
final class ChirpData extends DataResource
{
public static array $authorizations = [
'comment',
'like',
'unlike',
'delete'
];
public function __construct(
public readonly string $id,
public readonly ?string $body,
public readonly int $likes_count,
public readonly int $comments_count,
public readonly Carbon $created_at,
) {}
}
When transforming a data resource, a lazy authorization
property will be appended to the resulting array.
This property will contain a key for each defined policy action, and will be evaluated through Gate::allows
:
{
"id": "1514",
"body": "Ad nihil provident rem voluptatem quis modi harum ad. Tenetur sunt nisi libero qui debitis.",
"likes_count": 1,
"comments_count": 3,
"created_at": "2022-10-12T17:44:05+00:00",
"authorization": {
"comment": false,
"like": false,
"unlike": true,
"delete": false
}
}
The policy for the previous example could look like that:
use App\Models\Chirp;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class ChirpPolicy
{
use HandlesAuthorization;
public function comment(User $user): bool
{
return true;
}
public function delete(User $user, Chirp $chirp): bool
{
return $chirp->author->is($user);
}
public function like(User $user, Chirp $chirp): bool
{
return !$user->hasLiked($chirp);
}
public function unlike(User $user, Chirp $chirp): bool
{
return $user->hasLiked($chirp);
}
}
Authorizing on the front-end
When sharing a property from a data resource to the front-end, authorizations could directly be checked against the data object, but the can
util provides a better syntax.
import { can } from 'hybridly'
const $props = defineProps<{
chirp: App.Data.ChirpData
}>()
// With the `can` util (recommended)
const canComment = can($props.chirp, 'comment')
// As-is
const canComment = $props.chirp.authorization.comment
Avoid processing authorizations
The method Hybridly uses to provide automatic authorizations has a drawback: the gate is called for each action, each time the data object is serialized.
Fortunately, this is built on top of laravel-data
's lazy properties, which mean you can simply call ->exclude('authorization')
for them to not be processed.
public function show(Chirp $chirp)
{
$this->authorize('view', $chirp);
return hybridly('chirps.show', [
'chirp' => ChirpData::from($chirp)->exclude('authorization'),
]);
}
Using custom creation methods
When using a custom from
method, the pipeline that resolves authorizations will not be used.
Because of this, you will have to manually call the static resolveAuthorizationArray
method when instanciating your data object:
final class ChirpData extends DataResource
{
public static array $authorizations = [
'comment',
'like',
'unlike',
'delete'
];
public function __construct(
public readonly string $body,
) {
}
public static function fromModel(Chirp $chirp): static
{
return self::factory()
->withoutMagicalCreation()
->from([
'body' => $chirp->body,
'authorization' => static::resolveAuthorizationArray($chirp),
]);
}
}
You may wrap the authorization array in a Lazy
property if needed:
Lazy::create(fn () => static::resolveAuthorizationArray($chirp))
->defaultIncluded();