Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Http/Controllers/Internal/v1/AdminMetricsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Fleetbase\Models\Activity;
use Fleetbase\Models\Company;
use Fleetbase\Models\User;
use Fleetbase\Support\Auth;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
Expand All @@ -14,6 +15,18 @@

class AdminMetricsController extends Controller
{
public function __construct()
{
// Platform-wide figures: system administrators only.
$this->middleware(function ($request, $next) {
if (!Auth::getUserFromSession($request)?->isAdmin()) {
return response()->error('Only system administrators can view platform metrics.', 401);
}

return $next($request);
});
}

public function kpi(Request $request, string $slug): JsonResponse
{
[$currentPeriodStart, $previousPeriodStart] = $this->periodBoundaries();
Expand Down
5 changes: 5 additions & 0 deletions src/Http/Controllers/Internal/v1/ApiCredentialController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ class ApiCredentialController extends FleetbaseController
*/
public $service = 'developers';

/**
* The IAM schema resource this controller's permissions use.
*/
public string $permissionResource = 'api-key';

/**
* Create a new API credential record.
*
Expand Down
5 changes: 5 additions & 0 deletions src/Http/Controllers/Internal/v1/ApiEventController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ class ApiEventController extends FleetbaseController
* @var string
*/
public $service = 'developers';

/**
* The IAM schema resource this controller's permissions use.
*/
public string $permissionResource = 'event';
}
5 changes: 5 additions & 0 deletions src/Http/Controllers/Internal/v1/ApiRequestLogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ class ApiRequestLogController extends FleetbaseController
* @var string
*/
public $service = 'developers';

/**
* The IAM schema resource this controller's permissions use.
*/
public string $permissionResource = 'log';
}
46 changes: 46 additions & 0 deletions src/Http/Controllers/Internal/v1/CompanyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ class CompanyController extends FleetbaseController
*/
public $resource = 'company';

/**
* Company attributes only platform administrators may change through updateRecord().
*/
private const PLATFORM_MANAGED_FIELDS = ['owner_uuid', 'stripe_customer_id', 'stripe_connect_id', 'plan', 'status', 'trial_ends_at', 'type'];

public function __construct()
{
parent::__construct();

// Organization settings and the organization's 2FA policy: owner, Administrator role or system admin.
$this->middleware(function ($request, $next) {
$company = Company::where('uuid', session('company'))->first();
if (!$company || !$this->currentUserManagesOrganization($company)) {
return response()->error('Only the organization owner or an Administrator can change organization settings.', 401);
}

return $next($request);
})->only(['updateRecord', 'saveTwoFactorSettings']);
}

/**
* Find an organization visible to the current session company.
*
Expand Down Expand Up @@ -65,6 +85,11 @@ public function updateRecord(Request $request, string $id)

try {
$input = $this->model->getApiPayloadFromRequest($request);

// Ownership moves through transferOwnership(); billing and lifecycle fields are platform-managed.
if (!$request->user()?->isAdmin()) {
$input = Arr::except($input, self::PLATFORM_MANAGED_FIELDS);
}
$input = $this->model->fillSessionAttributes($input, [], ['updated_by_uuid']);

if ($this->model->isColumn('slug')) {
Expand Down Expand Up @@ -159,6 +184,7 @@ public function saveTwoFactorSettings(Request $request)
if (!$company) {
return response()->error('No company session found', 401);
}

if (isset($twoFaSettings['enabled']) && $twoFaSettings['enabled'] === false) {
$twoFaSettings['enforced'] = false;
}
Expand Down Expand Up @@ -320,6 +346,26 @@ private function resolveVisibleCompanyForUsers(string $id, Request $request): ?C
->first();
}

/**
* Whether the session user may manage the organization: platform admins, the owner,
* and members holding the Administrator role in it.
*/
private function currentUserManagesOrganization(Company $company): bool
{
$user = Auth::getUserFromSession();
if (!$user) {
return false;
}

if ($user->isAdmin() || $company->owner_uuid === $user->uuid) {
return true;
}

$companyUser = CompanyUser::where('company_uuid', $company->uuid)->where('user_uuid', $user->uuid)->first();

return $companyUser !== null && $companyUser->roles()->where('name', 'Administrator')->exists();
}

private function resolveVisibleCompany(string $id): ?Company
{
$sessionCompany = session('company');
Expand Down
13 changes: 13 additions & 0 deletions src/Http/Controllers/Internal/v1/DeveloperMetricsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,25 @@
use Fleetbase\Models\ApiRequestLog;
use Fleetbase\Models\WebhookEndpoint;
use Fleetbase\Models\WebhookRequestLog;
use Fleetbase\Support\Auth;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;

class DeveloperMetricsController extends Controller
{
public function __construct()
{
// Not a resource controller, so AuthorizationGuard cannot resolve a permission for it.
$this->middleware(function ($request, $next) {
if (Auth::cannotUnlessAdmin('developers list api-key')) {
return response()->error('User is not authorized to list api-key', 401);
}

return $next($request);
});
}

public function kpis(Request $request): JsonResponse
{
[$start, $end, $previousStart, $previousEnd] = $this->periods($request);
Expand Down
13 changes: 13 additions & 0 deletions src/Http/Controllers/Internal/v1/IamMetricsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Fleetbase\Models\Role;
use Fleetbase\Models\Setting;
use Fleetbase\Models\User;
use Fleetbase\Support\Auth;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
Expand All @@ -21,6 +22,18 @@ class IamMetricsController extends Controller
{
private const DORMANT_DAYS = 90;

public function __construct()
{
// Not a resource controller, so AuthorizationGuard cannot resolve a permission for it.
$this->middleware(function ($request, $next) {
if (Auth::cannotUnlessAdmin('iam list user')) {
return response()->error('User is not authorized to list user', 401);
}

return $next($request);
});
}

public function kpis(Request $request): JsonResponse
{
$companyUuid = session('company');
Expand Down
23 changes: 23 additions & 0 deletions src/Http/Controllers/Internal/v1/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace Fleetbase\Http\Controllers\Internal\v1;

use Fleetbase\Attributes\SkipAuthorizationCheck;
use Fleetbase\Http\Controllers\FleetbaseController;
use Fleetbase\Models\Report;
use Fleetbase\Support\Auth;
use Fleetbase\Support\Reporting\ComputedColumnValidator;
use Fleetbase\Support\Reporting\ReportQueryConverter;
use Fleetbase\Support\Reporting\ReportQueryErrorHandler;
Expand All @@ -22,12 +24,30 @@ class ReportController extends FleetbaseController
*/
public $resource = 'report';

/**
* The IAM schema service for report permissions (`iam list report`, `iam execute report`, ...).
*
* @var string
*/
public $service = 'iam';

protected ReportQueryValidator $queryValidator;
protected ReportQueryErrorHandler $errorHandler;

public function __construct()
{
parent::__construct();

// These endpoints skip AuthorizationGuard (their method names do not map to a schema action).
foreach (['execute' => ['executeQuery'], 'export' => ['exportQuery', 'download']] as $action => $methods) {
$this->middleware(function ($request, $next) use ($action) {
if (Auth::cannotUnlessAdmin("iam {$action} report")) {
return response()->error("User is not authorized to {$action} report", 401);
}

return $next($request);
})->only($methods);
}
$this->queryValidator = new ReportQueryValidator(app(ReportSchemaRegistry::class));
$this->errorHandler = new ReportQueryErrorHandler();
}
Expand Down Expand Up @@ -238,6 +258,7 @@ public function execute(Request $request, string $id): JsonResponse
/**
* Execute a query directly without saving as report.
*/
#[SkipAuthorizationCheck]
public function executeQuery(Request $request): JsonResponse
{
try {
Expand Down Expand Up @@ -368,6 +389,7 @@ public function export(Request $request, string $id): JsonResponse
/**
* Export query results directly without saving as report.
*/
#[SkipAuthorizationCheck]
public function exportQuery(Request $request): JsonResponse
{
try {
Expand Down Expand Up @@ -431,6 +453,7 @@ public function exportQuery(Request $request): JsonResponse
/**
* Download exported file.
*/
#[SkipAuthorizationCheck]
public function download(Request $request, string $filename)
{
try {
Expand Down
12 changes: 12 additions & 0 deletions src/Http/Controllers/Internal/v1/TwoFaController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
*/
class TwoFaController extends Controller
{
public function __construct()
{
// The system-wide 2FA policy applies to every organization: system administrators only.
$this->middleware(function ($request, $next) {
if (!Auth::getUserFromSession($request)?->isAdmin()) {
return response()->error('Only system administrators can change the system two-factor policy.', 401);
}

return $next($request);
})->only('saveSystemConfig');
}

/**
* Save Two-Factor Authentication system wide settings.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class WebhookEndpointController extends FleetbaseController
*/
public $service = 'developers';

/**
* The IAM schema resource this controller's permissions use.
*/
public string $permissionResource = 'webhook';

/**
* Enables a webhook endpoint.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ class WebhookRequestLogController extends FleetbaseController
* @var string
*/
public $service = 'developers';

/**
* The IAM schema resource this controller's permissions use.
*/
public string $permissionResource = 'log';
}
38 changes: 36 additions & 2 deletions src/Support/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ public static function resolvePermissionsFromRequest(Request $request): Collecti
}

$service = $controller->getService();
$resource = str_replace('_', '-', $controller->getResourceSingularName());
$resource = static::getPermissionResourceFromController($controller);
$action = ActionMapper::resolve($request, $resource);

// If the resource is not guarded at all
Expand Down Expand Up @@ -588,12 +588,24 @@ public static function applyDirectivesToQuery($builder, ?Request $request = null
public static function getRequiredPermissionNameFromRequest(Request $request): string
{
$controller = $request->getController();
$resource = str_replace('_', '-', $controller->getResourceSingularName());
$resource = static::getPermissionResourceFromController($controller);
$action = ActionMapper::resolve($request, $resource);

return implode(' ', [$action, $resource]);
}

/**
* Resolves the permission resource name for a resource controller.
*/
public static function getPermissionResourceFromController($controller): string
{
if (method_exists($controller, 'getPermissionResourceName')) {
return $controller->getPermissionResourceName();
}

return str_replace('_', '-', $controller->getResourceSingularName());
}

/**
* Checks if a resource is guarded by any permissions.
*
Expand Down Expand Up @@ -635,6 +647,28 @@ public static function can(string $permission): bool
});
}

/**
* Determines if the current user lacks the specified permission, treating platform
* administrators (and a missing session user) the way AuthorizationGuard does.
*
* For explicit checks in controllers that the guard cannot resolve on its own.
*
* @param string $permission the permission string in the format '{service} {action} {resource}'
*/
public static function cannotUnlessAdmin(string $permission): bool
{
$user = static::getUserFromSession();
if (!$user) {
return true;
}

if ($user->isAdmin()) {
return false;
}

return static::cannot($permission);
}

/**
* Determines if the current user lacks the specified permission.
*
Expand Down
16 changes: 16 additions & 0 deletions src/Traits/HasApiControllerBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,22 @@ public function getResourceSingularName(): string
return $this->resourceSingularlName;
}

/**
* Gets the resource name used in permission names, e.g. "user" in "iam list user".
*
* Defaults to the kebab-cased singular resource name. A controller whose model name
* differs from its IAM schema resource declares `public string $permissionResource`
* (for example ApiCredentialController uses the schema resource "api-key").
*/
public function getPermissionResourceName(): string
{
if (property_exists($this, 'permissionResource') && !empty($this->permissionResource)) {
return $this->permissionResource;
}

return str_replace('_', '-', $this->getResourceSingularName());
}

/**
* Gets the service associated with the controller.
*
Expand Down
Loading
Loading