Skip to content
Open
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
31 changes: 22 additions & 9 deletions ProcessMaker/Http/Controllers/Api/PermissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use ProcessMaker\Events\PermissionChanged;
use Illuminate\Validation\ValidationException;
use ProcessMaker\Events\PermissionUpdated;
use ProcessMaker\Http\Controllers\Controller;
use ProcessMaker\Http\Resources\ApiCollection;
use ProcessMaker\Models\Group;
use ProcessMaker\Models\Permission;
use ProcessMaker\Models\User;
Expand All @@ -30,7 +28,7 @@ class PermissionController extends Controller
*
* @param Request $request
*
* @return Response
* @return \Illuminate\Support\Collection
*/
public function index(Request $request)
{
Expand All @@ -44,7 +42,7 @@ public function index(Request $request)
*
* @param Request $request
*
* @return Response
* @return \Illuminate\Http\Response
*
* @OA\Put(
* path="/permissions",
Expand Down Expand Up @@ -82,8 +80,22 @@ public function index(Request $request)
*/
public function update(Request $request)
{
$request->validate([
'user_id' => 'required_without:group_id|integer',
'group_id' => 'required_without:user_id|integer',
'permission_names' => 'nullable|array',
]);

if ($request->filled('user_id') && $request->filled('group_id')) {
throw ValidationException::withMessages([
'user_id' => [__('The user_id field cannot be present when group_id is present.')],
'group_id' => [__('The group_id field cannot be present when user_id is present.')],
]);
}

//Obtain the requested user or group
if ($request->input('user_id')) {
if ($request->filled('user_id')) {
$this->authorize('edit-users');
$entity = User::findOrFail($request->input('user_id'));
// Obtain user old Permissions before save
$originalPermissionNames = $entity->permissions()->pluck('name')->toArray();
Expand All @@ -98,14 +110,15 @@ public function update(Request $request)
$entity->is_administrator = $isSettingToAdmin;
$entity->save();
}
} elseif ($request->input('group_id')) {
} elseif ($request->filled('group_id')) {
$this->authorize('edit-groups');
$entity = Group::findOrFail($request->input('group_id'));
// Obtain group old Permissions before save
$originalPermissionNames = $entity->permissions()->pluck('name')->toArray();
}

// Obtain the requested permission names for that entity
$requestPermissions = $request->input('permission_names');
$requestPermissions = $request->input('permission_names') ?? [];

// Convert permission names into a collection of Permission models
$permissions = Permission::whereIn('name', $requestPermissions)->get();
Expand All @@ -114,7 +127,7 @@ public function update(Request $request)
PermissionUpdated::dispatch(
$requestPermissions,
$originalPermissionNames,
$entity->is_administrator ?: false,
$entity instanceof User ? $entity->is_administrator : false,
$request->input('user_id'),
$request->input('group_id')
);
Expand Down
2 changes: 1 addition & 1 deletion routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@

// Permissions
Route::get('permissions', [PermissionController::class, 'index'])->name('permissions.index');
Route::put('permissions', [PermissionController::class, 'update'])->name('permissions.update')->middleware('can:edit-users');
Route::put('permissions', [PermissionController::class, 'update'])->name('permissions.update');

// Tenant Jobs Dashboard API
Route::get('tenant-queues/tenants', [TenantQueueController::class, 'getTenants'])->name('tenant-queue.tenants');
Expand Down
206 changes: 194 additions & 12 deletions tests/Feature/Api/PermissionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class PermissionsTest extends TestCase
{
use RequestHelper;

private const PERMISSIONS_URL = '/permissions';

private const PROCESSES_URL = '/processes';

protected function withUserSetup()
{
$this->user->is_administrator = false;
Expand Down Expand Up @@ -59,10 +63,10 @@ public function testApiPermissions()
'status' => 'ACTIVE',
]);

$response = $this->apiCall('GET', '/processes');
$response = $this->apiCall('GET', self::PROCESSES_URL);
$response->assertStatus(200);

$response = $this->apiCall('GET', '/processes/' . $process->id);
$response = $this->apiCall('GET', self::PROCESSES_URL . '/' . $process->id);
$response->assertStatus(200);

$permission = Permission::byName('archive-processes');
Expand All @@ -74,7 +78,7 @@ public function testApiPermissions()
// Invalidate permission cache to ensure changes take effect
$this->user->invalidatePermissionCache();

$response = $this->apiCall('DELETE', '/processes/' . $process->id);
$response = $this->apiCall('DELETE', self::PROCESSES_URL . '/' . $process->id);
$response->assertStatus(403);

$this->user->permissions()->attach($permission->id);
Expand All @@ -84,7 +88,7 @@ public function testApiPermissions()
// Invalidate permission cache to ensure the new permission takes effect
$this->user->invalidatePermissionCache();

$response = $this->apiCall('DELETE', '/processes/' . $process->id);
$response = $this->apiCall('DELETE', self::PROCESSES_URL . '/' . $process->id);
$response->assertStatus(204);
}

Expand All @@ -97,7 +101,7 @@ public function testSetPermissionsForUser()

$testUser = User::factory()->create();
$testPermission = Permission::factory()->create();
$response = $this->apiCall('PUT', '/permissions', [
$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'user_id' => $testUser->id,
'permission_names' => [$testPermission->name],
]);
Expand All @@ -109,6 +113,184 @@ public function testSetPermissionsForUser()
$this->assertEquals($testUser->permissions->first()->id, $testPermission->id);
}

public function testSetPermissionsForGroupWithInheritedEditGroupsPermission()
{
$this->user = User::factory()->create([
'password' => Hash::make('password'),
'is_administrator' => false,
]);
$this->initializePermissions(false);

$adminGroup = Group::factory()->create();
$adminGroup->permissions()->attach(Permission::whereIn('name', [
'view-groups',
'create-groups',
'edit-groups',
'delete-groups',
])->pluck('id'));

GroupMember::factory()->create([
'group_id' => $adminGroup->id,
'member_type' => User::class,
'member_id' => $this->user->id,
]);

$this->user->invalidatePermissionCache();

$targetGroup = Group::factory()->create();

$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'group_id' => $targetGroup->id,
'permission_names' => ['view-groups', 'edit-groups'],
]);

$response->assertStatus(204);
$this->assertEqualsCanonicalizing(
['view-groups', 'edit-groups'],
$targetGroup->refresh()->permissions()->pluck('name')->toArray()
);
}

public function testSetPermissionsForUserWithInheritedEditUsersPermission()
{
$this->user = User::factory()->create([
'password' => Hash::make('password'),
'is_administrator' => false,
]);
$this->initializePermissions(false);

$adminGroup = Group::factory()->create();
$adminGroup->permissions()->attach(Permission::byName('edit-users')->id);

GroupMember::factory()->create([
'group_id' => $adminGroup->id,
'member_type' => User::class,
'member_id' => $this->user->id,
]);

$this->user->invalidatePermissionCache();

$targetUser = User::factory()->create();

$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'user_id' => $targetUser->id,
'permission_names' => ['view-groups'],
]);

$response->assertStatus(204);
$this->assertEqualsCanonicalizing(
['view-groups'],
$targetUser->refresh()->permissions()->pluck('name')->toArray()
);
}

public function testSetPermissionsForUserRequiresEditUsersPermission()
{
$this->user = User::factory()->create([
'password' => Hash::make('password'),
'is_administrator' => false,
]);
$this->initializePermissions(false);

$adminGroup = Group::factory()->create();
$adminGroup->permissions()->attach(Permission::byName('edit-groups')->id);

GroupMember::factory()->create([
'group_id' => $adminGroup->id,
'member_type' => User::class,
'member_id' => $this->user->id,
]);

$this->user->invalidatePermissionCache();

$targetUser = User::factory()->create();

$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'user_id' => $targetUser->id,
'permission_names' => ['view-groups'],
]);

$response->assertStatus(403);
$this->assertFalse($targetUser->refresh()->permissions()->where('name', 'view-groups')->exists());
}

public function testSetPermissionsForGroupRequiresEditGroupsPermission()
{
$this->user = User::factory()->create([
'password' => Hash::make('password'),
'is_administrator' => false,
]);
$this->initializePermissions(false);

$targetGroup = Group::factory()->create();

$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'group_id' => $targetGroup->id,
'permission_names' => ['view-groups'],
]);

$response->assertStatus(403);
$this->assertCount(0, $targetGroup->refresh()->permissions);
}

public function testUnauthorizedPermissionUpdatesDoNotExposeTargetExistence()
{
$this->user = User::factory()->create([
'password' => Hash::make('password'),
'is_administrator' => false,
]);
$this->initializePermissions(false);

$targetUser = User::factory()->create();
$targetGroup = Group::factory()->create();
$missingUserId = User::max('id') + 1;
$missingGroupId = Group::max('id') + 1;

$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'user_id' => $targetUser->id,
'permission_names' => ['view-groups'],
]);
$response->assertStatus(403);

$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'user_id' => $missingUserId,
'permission_names' => ['view-groups'],
]);
$response->assertStatus(403);

$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'group_id' => $targetGroup->id,
'permission_names' => ['view-groups'],
]);
$response->assertStatus(403);

$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'group_id' => $missingGroupId,
'permission_names' => ['view-groups'],
]);
$response->assertStatus(403);
}

public function testSetPermissionsRequiresExactlyOneTarget()
{
$targetUser = User::factory()->create();
$targetGroup = Group::factory()->create();

$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'permission_names' => ['view-groups'],
]);

$response->assertStatus(422);

$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'user_id' => $targetUser->id,
'group_id' => $targetGroup->id,
'permission_names' => ['view-groups'],
]);

$response->assertStatus(422);
}

public function testSetPermissionsViewProcessCatalogForUser()
{
$faker = Faker::create();
Expand Down Expand Up @@ -180,7 +362,7 @@ public function testCategoryPermission()
// Invalidate permission cache to ensure the new permission takes effect
$this->user->invalidatePermissionCache();

$response = $this->apiCall('PUT', $url, $attrs);
$this->apiCall('PUT', $url, $attrs);
$this->assertEquals('Test Category Update', $class::find($id)->name);

// test view permission
Expand Down Expand Up @@ -250,8 +432,8 @@ public function testSetPermissionsViewMyRequestForUser()
public function testSetPermissionsViewMyRequestForUsersAndGroupCreated()
{
// Set up the users and groups
$users = User::factory()->count(5)->create();
$groups = Group::factory()->count(3)->create();
User::factory()->count(5)->create();
Group::factory()->count(3)->create();

// Run the seeder
$this->seed(PermissionSeeder::class);
Expand Down Expand Up @@ -279,7 +461,7 @@ public function testAdministratorRoleAssignment()
$this->user = $regularUser;
$this->user->save();

$response = $this->apiCall('PUT', '/permissions', [
$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'user_id' => $targetUser->id,
'is_administrator' => true,
'permission_names' => [],
Expand All @@ -292,7 +474,7 @@ public function testAdministratorRoleAssignment()
$targetUser->is_administrator = true;
$targetUser->save();

$response = $this->apiCall('PUT', '/permissions', [
$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'user_id' => $targetUser->id,
'is_administrator' => false,
'permission_names' => [],
Expand All @@ -306,7 +488,7 @@ public function testAdministratorRoleAssignment()
$this->user = $adminUser;
$this->user->save();

$response = $this->apiCall('PUT', '/permissions', [
$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'user_id' => $targetUser->id,
'is_administrator' => false,
'permission_names' => [],
Expand All @@ -317,7 +499,7 @@ public function testAdministratorRoleAssignment()
$this->assertFalse($targetUser->is_administrator);

// Test 4: Admin can grant admin role
$response = $this->apiCall('PUT', '/permissions', [
$response = $this->apiCall('PUT', self::PERMISSIONS_URL, [
'user_id' => $targetUser->id,
'is_administrator' => true,
'permission_names' => [],
Expand Down
Loading