Skip to content

Commit

Permalink
implementing authorization via permissions, policies, roles
Browse files Browse the repository at this point in the history
  • Loading branch information
roncodes committed Aug 13, 2024
1 parent f6a1345 commit 6677938
Show file tree
Hide file tree
Showing 25 changed files with 813 additions and 140 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fleetbase/core-api",
"version": "1.5.1",
"version": "1.5.2",
"description": "Core Framework and Resources for Fleetbase API",
"keywords": [
"fleetbase",
Expand Down
48 changes: 8 additions & 40 deletions seeders/PermissionSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Fleetbase\Models\Policy;
use Fleetbase\Support\Utils;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

Expand All @@ -21,6 +22,9 @@ public function run()
Schema::disableForeignKeyConstraints();
Permission::truncate();
Policy::truncate();
DB::table('model_has_permissions')->truncate();
DB::table('model_has_roles')->truncate();
DB::table('model_has_policies')->truncate();

$actions = ['create', 'update', 'delete', 'view', 'list'];
$schemas = Utils::getAuthSchemas();
Expand All @@ -29,7 +33,7 @@ public function run()
$service = $schema->name;
$resources = $schema->resources ?? [];
$permissions = $schema->permissions ?? null;
$guard = 'web';
$guard = 'sanctum';

// first create a wilcard permission for the entire schema
$administratorPolicy = Policy::firstOrCreate(
Expand Down Expand Up @@ -103,44 +107,18 @@ public function run()
// create a resource policy for read-only access
$readOnlyPolicy = Policy::firstOrCreate(
[
'name' => Str::studly(data_get($schema, 'policyName')) . 'FullAccess',
'name' => Str::studly(data_get($schema, 'policyName')) . 'ReadOnly',
'guard_name' => $guard,
],
[
'name' => Str::studly(data_get($schema, 'policyName')) . 'FullAccess',
'name' => Str::studly(data_get($schema, 'policyName')) . 'ReadOnly',
'description' => 'Provides read-only access to ' . Str::studly(data_get($schema, 'policyName')) . '.',
'guard_name' => $guard,
]
);

// create wilcard permission for service and all resources
foreach ($resources as $resource) {
// create a resource policy for full access
$resourceFullAccessPolicy = Policy::firstOrCreate(
[
'name' => Str::studly(data_get($schema, 'policyName')) . Str::studly(data_get($resource, 'name')) . 'FullAccess',
'guard_name' => $guard,
],
[
'name' => Str::studly(data_get($schema, 'policyName')) . Str::studly(data_get($resource, 'name')) . 'FullAccess',
'description' => 'Provides full access to ' . Str::studly(data_get($schema, 'policyName')) . ' ' . Str::plural(data_get($resource, 'name')) . '.',
'guard_name' => $guard,
]
);

// create a resource policy for read-only access
$resourceReadOnlyPolicy = Policy::firstOrCreate(
[
'name' => Str::studly(data_get($schema, 'policyName')) . Str::studly(data_get($resource, 'name')) . 'FullAccess',
'guard_name' => $guard,
],
[
'name' => Str::studly(data_get($schema, 'policyName')) . Str::studly(data_get($resource, 'name')) . 'FullAccess',
'description' => 'Provides read-only access to ' . Str::studly(data_get($schema, 'policyName')) . ' ' . Str::plural(data_get($resource, 'name')) . '.',
'guard_name' => $guard,
]
);

$permission = Permission::firstOrCreate(
[
'name' => $service . ' * ' . data_get($resource, 'name'),
Expand All @@ -158,11 +136,6 @@ public function run()
} catch (\Spatie\Permission\Exceptions\GuardDoesNotMatch $e) {
dd($e->getMessage());
}
try {
$resourceFullAccessPolicy->givePermissionTo($permission);
} catch (\Spatie\Permission\Exceptions\GuardDoesNotMatch $e) {
dd($e->getMessage());
}

// output message for permissions creation
// $this->output('Created (' . $guard . ') permission: ' . $permission->name);
Expand All @@ -174,7 +147,7 @@ public function run()
if (is_array(data_get($resource, 'remove_actions', null))) {
foreach (data_get($resource, 'remove_actions') as $remove) {
if (($key = array_search($remove, $actions)) !== false) {
unset($actions[$key]);
unset($resourceActions[$key]);
}
}
}
Expand All @@ -199,11 +172,6 @@ public function run()
} catch (\Spatie\Permission\Exceptions\GuardDoesNotMatch $e) {
dd($e->getMessage());
}
try {
$resourceReadOnlyPolicy->givePermissionTo($permission);
} catch (\Spatie\Permission\Exceptions\GuardDoesNotMatch $e) {
dd($e->getMessage());
}
}

// output message for permissions creation
Expand Down
8 changes: 8 additions & 0 deletions src/Attributes/SkipAuthorizationCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Fleetbase\Attributes;

#[\Attribute(\Attribute::TARGET_METHOD)]
class SkipAuthorizationCheck
{
}
10 changes: 5 additions & 5 deletions src/Contracts/Policy.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
interface Policy
{
/**
* A role may be given various permissions.
* A policy may be given various permissions.
*/
public function permissions(): BelongsToMany;

/**
* Find a role by its name and guard name.
* Find a policy by its name and guard name.
*
* @param string|null $guardName
*
Expand All @@ -23,18 +23,18 @@ public function permissions(): BelongsToMany;
public static function findByName(string $name, $guardName): self;

/**
* Find a role by its id and guard name.
* Find a policy by its id and guard name.
*
* @param string|null $guardName
*
* @return \Fleebase\Policy
*
* @throws \Fleetbase\Exceptions\PolicyDoesNotExist
*/
public static function findById(int $id, $guardName): self;
public static function findById(string $id, $guardName): self;

/**
* Find or create a role by its name and guard name.
* Find or create a policy by its name and guard name.
*
* @param string|null $guardName
*
Expand Down
28 changes: 28 additions & 0 deletions src/Exceptions/UnauthorizedRequestException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Fleetbase\Exceptions;

use Fleetbase\Support\Auth;
use Illuminate\Http\Request;

class UnauthorizedRequestException extends \Exception implements \Throwable
{
protected array $errors = [];

public function __construct(Request $request, $code = 0, ?\Throwable $previous = null)
{
$message = $this->getErrorMessage($request);
$this->errors = [$message];
parent::__construct($message, $code, $previous);
}

public function getErrorMessage(Request $request): string
{
$requiredPermission = Auth::getRequiredPermissionNameFromRequest($request);
if (!$requiredPermission) {
return 'Unauthorized Request';
}

return 'User is not authorized to ' . $requiredPermission;
}
}
38 changes: 23 additions & 15 deletions src/Expansions/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use CompressJson\Core\Compressor;
use Fleetbase\Build\Expansion;
use Fleetbase\Support\Auth;
use Illuminate\Support\MessageBag;

class Response implements Expansion
Expand All @@ -19,19 +20,12 @@ public static function target()
}

/**
* Iterates request params until a param is found.
* Responds with a Fleetbase compatible error response.
*
* @return Closure
*/
public function error()
{
/*
* Returns an error response.
*
* @param array $params
* @param mixed $default
* @return mixed
*/
return function ($error, int $statusCode = 400, ?array $data = []) {
if ($error instanceof MessageBag) {
$error = $error->all();
Expand All @@ -47,20 +41,34 @@ public function error()
};
}

/**
* Responds with a Fleetbase compatible error response.
*
* @return Closure
*/
public function authorizationError()
{
return function (?array $data = []) {
/* @var \Illuminate\Support\Facades\Response $this */
$requiredPermission = Auth::getRequiredPermissionNameFromRequest(request());
$error = 'User is not authorized to ' . $requiredPermission;

return static::json(
array_merge([
'errors' => [$error],
], $data),
401
);
};
}

/**
* Formats a error response for the consumable API.
*
* @return Closure
*/
public function apiError()
{
/*
* Returns an error response.
*
* @param array $params
* @param mixed $default
* @return mixed
*/
return function ($error, int $statusCode = 400, ?array $data = []) {
if ($error instanceof MessageBag) {
$error = $error->all();
Expand Down
7 changes: 7 additions & 0 deletions src/Http/Controllers/Internal/v1/GroupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ class GroupController extends FleetbaseController
*/
public $resource = 'group';

/**
* The service which this controller belongs to.
*
* @var string
*/
public $service = 'iam';

/**
* Creates a record with request payload.
*
Expand Down
7 changes: 7 additions & 0 deletions src/Http/Controllers/Internal/v1/PolicyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ class PolicyController extends FleetbaseController
*/
public $resource = 'policy';

/**
* The service which this controller belongs to.
*
* @var string
*/
public $service = 'iam';

/**
* Creates a record by an identifier with request payload.
*
Expand Down
22 changes: 22 additions & 0 deletions src/Http/Controllers/Internal/v1/RoleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Fleetbase\Exceptions\FleetbaseRequestValidationException;
use Fleetbase\Http\Controllers\FleetbaseController;
use Fleetbase\Models\Permission;
use Fleetbase\Models\Policy;
use Illuminate\Http\Request;

class RoleController extends FleetbaseController
Expand All @@ -16,6 +17,13 @@ class RoleController extends FleetbaseController
*/
public $resource = 'role';

/**
* The service which this controller belongs to.
*
* @var string
*/
public $service = 'iam';

/**
* Creates a record by an identifier with request payload.
*
Expand All @@ -25,10 +33,17 @@ public function createRecord(Request $request)
{
try {
$record = $this->model->createRecordFromRequest($request, null, function ($request, &$role) {
// Sync Permissions
if ($request->isArray('role.permissions')) {
$permissions = Permission::whereIn('id', $request->array('role.permissions'))->get();
$role->syncPermissions($permissions);
}

// Sync Policies
if ($request->isArray('role.policies')) {
$policies = Policy::whereIn('id', $request->array('role.policies'))->get();
$role->syncPolicies($policies);
}
});

return ['role' => new $this->resource($record)];
Expand All @@ -50,10 +65,17 @@ public function updateRecord(Request $request, string $id)
{
try {
$record = $this->model->updateRecordFromRequest($request, $id, function ($request, &$role) {
// Sync Permissions
if ($request->isArray('role.permissions')) {
$permissions = Permission::whereIn('id', $request->array('role.permissions'))->get();
$role->syncPermissions($permissions);
}

// Sync Policies
if ($request->isArray('role.policies')) {
$policies = Policy::whereIn('id', $request->array('role.policies'))->get();
$role->syncPolicies($policies);
}
});

return ['role' => new $this->resource($record)];
Expand Down
Loading

0 comments on commit 6677938

Please sign in to comment.