Skip to content

Commit

Permalink
Merge pull request #118 from fleetbase/dev-v1.5.15
Browse files Browse the repository at this point in the history
v1.5.15
  • Loading branch information
roncodes authored Oct 17, 2024
2 parents 8dd4932 + 4916aff commit 8db60ca
Show file tree
Hide file tree
Showing 20 changed files with 416 additions and 36 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.14",
"version": "1.5.15",
"description": "Core Framework and Resources for Fleetbase API",
"keywords": [
"fleetbase",
Expand Down
54 changes: 54 additions & 0 deletions migrations/2024_10_17_075756_add_access_token_id_to_log_tables.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class() extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::disableForeignKeyConstraints();

Schema::table('api_events', function (Blueprint $table) {
$table->foreignId('access_token_id')->nullable()->after('api_credential_uuid')->references(['id'])->on('personal_access_tokens')->onUpdate('CASCADE')->onDelete('CASCADE');
});

Schema::table('webhook_request_logs', function (Blueprint $table) {
$table->foreignId('access_token_id')->nullable()->after('api_credential_uuid')->references(['id'])->on('personal_access_tokens')->onUpdate('CASCADE')->onDelete('CASCADE');
});

Schema::table('api_request_logs', function (Blueprint $table) {
$table->foreignId('access_token_id')->nullable()->after('api_credential_uuid')->references(['id'])->on('personal_access_tokens')->onUpdate('CASCADE')->onDelete('CASCADE');
});

Schema::enableForeignKeyConstraints();
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::disableForeignKeyConstraints();

Schema::table('api_events', function (Blueprint $table) {
$table->dropForeign(['access_token_id']);
$table->dropColumn('access_token_id');
});

Schema::table('webhook_request_logs', function (Blueprint $table) {
$table->dropForeign(['access_token_id']);
$table->dropColumn('access_token_id');
});

Schema::table('api_request_logs', function (Blueprint $table) {
$table->dropForeign(['access_token_id']);
$table->dropColumn('access_token_id');
});

Schema::enableForeignKeyConstraints();
}
};
2 changes: 1 addition & 1 deletion src/Auth/Schemas/IAM.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class IAM
],
[
'name' => 'user',
'actions' => ['deactivate', 'activate', 'verify', 'export'],
'actions' => ['deactivate', 'activate', 'verify', 'export', 'change-password-for'],
],
[
'name' => 'role',
Expand Down
107 changes: 105 additions & 2 deletions src/Console/Commands/Recovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

namespace Fleetbase\Console\Commands;

use Fleetbase\Mail\UserCredentialsMail;
use Fleetbase\Models\Company;
use Fleetbase\Models\Role;
use Fleetbase\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;

/**
Expand Down Expand Up @@ -73,6 +75,8 @@ public function handle()
'Set Role for User',
'Assign User to Company',
'Assign Owner to Company',
'Reset User Password',
'Set User as System Admin',
];

$action = $this->choice('Which recovery option would you like to perform?', $actions);
Expand Down Expand Up @@ -141,7 +145,7 @@ public function setRoleForUser(?User $user = null, ?Company $company = null)
}

$roleName = $this->anticipate('Input the role you wish to set for this user', function ($input) {
$results = Role::where(DB::raw('lower(name)'), 'like', '%' . str_replace('.', '%', str_replace(',', '%', $input)) . '%')->get();
$results = Role::where(DB::raw('lower(name)'), 'like', '%' . str_replace('.', '%', str_replace(',', '%', $input)) . '%')->whereNull('company_uuid')->get();

return $results->map(function ($role) {
return $role->name;
Expand All @@ -161,6 +165,105 @@ public function setRoleForUser(?User $user = null, ?Company $company = null)
$this->info('Done');
}

/**
* Promotes a user to a system administrator.
*
* This method assigns system administrator privileges to the specified user, granting them full access
* to the system, including sensitive configurations and secrets. If no user is provided, the method
* will prompt the administrator to select a user. A warning is displayed to inform about the implications
* of this action, and confirmation is required before proceeding.
*
* @param User|null $user The user to be promoted to system administrator. If null, the method will prompt for a user.
*
* @return void
*
* @throws \Exception if an error occurs while setting the user type
*/
public function setUserAsSystemAdmin(?User $user = null)
{
$user = $user ? $user : $this->promptForUser();
if (!$user) {
return $this->error('No user selected or found to make system admin.');
}

// User name output
$usernameOutput = $user->name . ' (' . $user->email . ')';

$this->warn('WARNING: By making a user a system administrator they will gain complete system access rights, including sensitive configurations and secrets. Run this command at your own risk.');
$confirm = $this->confirm('Are you sure you want to make ' . $usernameOutput . ' a system administrator?');

if ($confirm) {
try {
$user->setType('admin');
$this->info('User ' . $usernameOutput . ' is now a system administrator.');
} catch (\Throwable $e) {
$this->error($e->getMessage());
}
}

$this->info('Done');
}

/**
* Resets the password of a specified user.
*
* This method allows an administrator to reset a user's password. If no user is provided, the method
* will prompt to select one. It ensures that the new password is confirmed correctly and provides
* options to retry the reset if the passwords do not match. Additionally, it offers the option to
* send the new password to the user's email address.
*
* @param User|null $user The user whose password is to be reset. If null, the method will prompt for a user.
*
* @return void
*
* @throws \Exception if an error occurs while changing the user's password or sending the email
*/
public function resetUserPassword(?User $user = null)
{
$user = $user ? $user : $this->promptForUser();
if (!$user) {
return $this->error('No user selected or found to reset password for.');
}

// User name output
$usernameOutput = $user->name . ' (' . $user->email . ')';

// Inform
$this->info('Running password reset for user ' . $usernameOutput);

// Prompt for user password
$password = $this->secret('Enter the a new password');
$confirmPassword = $this->secret('Confirm the new password');

// Validate
if ($password !== $confirmPassword) {
$this->error('Passwords do not match.');
$retry = $this->confirm('Would you like to continue password reset for the user ' . $usernameOutput . '?');
if ($retry) {
return $this->resetUserPassword($user);
}

return;
}

$confirm = $this->confirm('Are you sure you want to reset the password');
$sendUserPassword = $this->confirm('Would you also like to send the users new password to their email (' . $user->email . ')?');

if ($confirm) {
try {
$user->changePassword($password);
if ($sendUserPassword) {
Mail::to($user)->send(new UserCredentialsMail($password, $user));
}
$this->info('User ' . $usernameOutput . ' password was changed.');
} catch (\Throwable $e) {
$this->error($e->getMessage());
}
}

$this->info('Done');
}

/**
* Assigns a user to a company with a specified role.
*
Expand Down Expand Up @@ -203,7 +306,7 @@ public function assignUserToCompany(?User $user = null, ?Company $company = null
}

$roleName = $this->anticipate('Input the role you wish to set for this user', function ($input) {
$results = Role::where(DB::raw('lower(name)'), 'like', '%' . str_replace('.', '%', str_replace(',', '%', $input)) . '%')->get();
$results = Role::where(DB::raw('lower(name)'), 'like', '%' . str_replace('.', '%', str_replace(',', '%', $input)) . '%')->whereNull('company_uuid')->get();

return $results->map(function ($role) {
return $role->name;
Expand Down
2 changes: 1 addition & 1 deletion src/Events/ResourceLifecycleEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ protected function addRelationshipSpecificChannels($model, &$channels)
{
$relationships = ['driverAssigned', 'customer', 'facilitator', 'vendor'];
foreach ($relationships as $relationship) {
if ($model && isset($model->{Str::slug($relationship) . '_uuid'})) {
if ($model && isset($model->{Str::snake($relationship) . '_uuid'})) {
$channels[] = new Channel($relationship . '.' . $model->{$relationship . '_uuid'});
}
if ($model && isset($model->{$relationship})) {
Expand Down
51 changes: 51 additions & 0 deletions src/Http/Controllers/Internal/v1/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Fleetbase\Http\Requests\SignUpRequest;
use Fleetbase\Http\Requests\SwitchOrganizationRequest;
use Fleetbase\Http\Resources\Organization;
use Fleetbase\Mail\UserCredentialsMail;
use Fleetbase\Models\Company;
use Fleetbase\Models\CompanyUser;
use Fleetbase\Models\Invite;
Expand All @@ -23,6 +24,7 @@
use Fleetbase\Twilio\Support\Laravel\Facade as Twilio;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;
use Laravel\Sanctum\PersonalAccessToken;
Expand Down Expand Up @@ -625,4 +627,53 @@ public function services()

return response()->json(array_unique($services));
}

/**
* Change a user password.
*
* @return \Illuminate\Http\Response
*/
public function changeUserPassword(Request $request)
{
$user = Auth::getUserFromSession($request);
if (!$user) {
return response()->error('Not authorized to change user password.', 401);
}

$canChangePassword = $user->isAdmin() || $user->hasRole('Administrator') || $user->hasPermissionTo('iam change-password-for user');
if (!$canChangePassword) {
return response()->error('Not authorized to change user password.', 401);
}

// Get request input
$userId = $request->input('user');
$password = $request->input('password');
$confirmPassword = $request->input('password_confirmation');
$sendCredentials = $request->boolean('send_credentials');

if (!$userId) {
return response()->error('No user specified to change password for.');
}

if ($password !== $confirmPassword) {
return response()->error('Passwords do not match.');
}

$targetUser = User::where('uuid', $userId)->whereHas('anyCompanyUser', function ($query) {
$query->where('company_uuid', session('company'));
})->first();
if (!$targetUser) {
return response()->error('User not found to change password for.');
}

// Change password
$targetUser->changePassword($password);

// Send credentials to customer if opted
if ($sendCredentials) {
Mail::to($targetUser)->send(new UserCredentialsMail($password, $targetUser));
}

return response()->json(['status' => 'ok']);
}
}
7 changes: 7 additions & 0 deletions src/Http/Controllers/Internal/v1/RoleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Fleetbase\Models\Permission;
use Fleetbase\Models\Policy;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class RoleController extends FleetbaseController
{
Expand All @@ -31,6 +32,12 @@ class RoleController extends FleetbaseController
*/
public function createRecord(Request $request)
{
// Disable ability to create any Administrator role
$roleName = strtolower($request->input('role.name', ''));
if ($roleName === 'administrator' || Str::startsWith($roleName, 'admin')) {
return response()->error('Creating a role with name "Administrator" or a role name that starts with "Admin" is prohibited, as the name is system reserved.');
}

try {
$record = $this->model->createRecordFromRequest($request, null, function ($request, &$role) {
// Sync Permissions
Expand Down
11 changes: 10 additions & 1 deletion src/Http/Middleware/SetupFleetbaseSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Fleetbase\Http\Middleware;

use Fleetbase\Support\Auth;
use Laravel\Sanctum\PersonalAccessToken;

class SetupFleetbaseSession
{
Expand All @@ -13,9 +14,17 @@ class SetupFleetbaseSession
*/
public function handle($request, \Closure $next)
{
Auth::setSession($request->user());
$user = $request->user();
Auth::setSession($user);
Auth::setSandboxSession($request);

if (method_exists($user, 'currentAccessToken')) {
$personalAccessToken = $user->currentAccessToken();
if ($personalAccessToken && $personalAccessToken instanceof PersonalAccessToken) {
Auth::setApiKey($personalAccessToken);
}
}

return $next($request);
}
}
15 changes: 11 additions & 4 deletions src/Jobs/LogApiRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Laravel\Sanctum\PersonalAccessToken;
use Spatie\ResponseCache\Facades\ResponseCache;

class LogApiRequest implements ShouldQueue
Expand Down Expand Up @@ -81,11 +82,17 @@ public static function getPayload(Request $request, $response): array
$related[] = Utils::get($content, 'id');
}

// Get api credential from session
$apiCredential = session('api_credential');

// Validate api credential, if not uuid then it could be internal
if (ApiCredential::where('uuid', session('api_credential'))->exists()) {
// Need to add a `api_credentail_type` field and morph -- in later versions
// As could be `PersonalAccessToken` `ApiCredential` and eventually `NavigatorAppToken`
$payload['api_credential_uuid'] = session('api_credential');
if ($apiCredential && Str::isUuid($apiCredential) && ApiCredential::where('uuid', session('api_credential'))->exists()) {
$payload['api_credential_uuid'] = $apiCredential;
}

// Check if it was a personal access token which made the request
if ($apiCredential && PersonalAccessToken::where('id', $apiCredential)->exists()) {
$payload['access_token_id'] = $apiCredential;
}

// Get request duration
Expand Down
Loading

0 comments on commit 8db60ca

Please sign in to comment.