Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User Roles API Endpoint #4051

Merged
merged 6 commits into from
Feb 19, 2023
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
54 changes: 27 additions & 27 deletions app/Auth/Permissions/PermissionsRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@
class PermissionsRepo
{
protected JointPermissionBuilder $permissionBuilder;
protected $systemRoles = ['admin', 'public'];
protected array $systemRoles = ['admin', 'public'];

/**
* PermissionsRepo constructor.
*/
public function __construct(JointPermissionBuilder $permissionBuilder)
{
$this->permissionBuilder = $permissionBuilder;
Expand All @@ -41,7 +38,7 @@ public function getAllRolesExcept(Role $role): Collection
/**
* Get a role via its ID.
*/
public function getRoleById($id): Role
public function getRoleById(int $id): Role
{
return Role::query()->findOrFail($id);
}
Expand All @@ -52,10 +49,10 @@ public function getRoleById($id): Role
public function saveNewRole(array $roleData): Role
{
$role = new Role($roleData);
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
$role->mfa_enforced = boolval($roleData['mfa_enforced'] ?? false);
$role->save();

$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$permissions = $roleData['permissions'] ?? [];
$this->assignRolePermissions($role, $permissions);
$this->permissionBuilder->rebuildForRole($role);

Expand All @@ -66,42 +63,45 @@ public function saveNewRole(array $roleData): Role

/**
* Updates an existing role.
* Ensure Admin role always have core permissions.
* Ensures Admin system role always have core permissions.
*/
public function updateRole($roleId, array $roleData)
public function updateRole($roleId, array $roleData): Role
{
$role = $this->getRoleById($roleId);

$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
if ($role->system_name === 'admin') {
$permissions = array_merge($permissions, [
'users-manage',
'user-roles-manage',
'restrictions-manage-all',
'restrictions-manage-own',
'settings-manage',
]);
if (isset($roleData['permissions'])) {
$this->assignRolePermissions($role, $roleData['permissions']);
}

$this->assignRolePermissions($role, $permissions);

$role->fill($roleData);
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
$role->save();
$this->permissionBuilder->rebuildForRole($role);

Activity::add(ActivityType::ROLE_UPDATE, $role);

return $role;
}

/**
* Assign a list of permission names to a role.
* Assign a list of permission names to the given role.
*/
protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
protected function assignRolePermissions(Role $role, array $permissionNameArray = []): void
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);

if ($permissionNameArray) {
// Ensure the admin system role retains vital system permissions
if ($role->system_name === 'admin') {
$permissionNameArray = array_unique(array_merge($permissionNameArray, [
'users-manage',
'user-roles-manage',
'restrictions-manage-all',
'restrictions-manage-own',
'settings-manage',
]));
}

if (!empty($permissionNameArray)) {
$permissions = RolePermission::query()
->whereIn('name', $permissionNameArray)
->pluck('id')
Expand All @@ -114,13 +114,13 @@ protected function assignRolePermissions(Role $role, array $permissionNameArray
/**
* Delete a role from the system.
* Check it's not an admin role or set as default before deleting.
* If an migration Role ID is specified the users assign to the current role
* If a migration Role ID is specified the users assign to the current role
* will be added to the role of the specified id.
*
* @throws PermissionsException
* @throws Exception
*/
public function deleteRole($roleId, $migrateRoleId)
public function deleteRole(int $roleId, int $migrateRoleId = 0): void
{
$role = $this->getRoleById($roleId);

Expand All @@ -131,7 +131,7 @@ public function deleteRole($roleId, $migrateRoleId)
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
}

if ($migrateRoleId) {
if ($migrateRoleId !== 0) {
$newRole = Role::query()->find($migrateRoleId);
if ($newRole) {
$users = $role->users()->pluck('id')->toArray();
Expand Down
2 changes: 2 additions & 0 deletions app/Auth/Permissions/RolePermission.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

/**
* @property int $id
* @property string $name
* @property string $display_name
*/
class RolePermission extends Model
{
Expand Down
6 changes: 5 additions & 1 deletion app/Auth/Role.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ class Role extends Model implements Loggable
{
use HasFactory;

protected $fillable = ['display_name', 'description', 'external_auth_id'];
protected $fillable = ['display_name', 'description', 'external_auth_id', 'mfa_enforced'];

protected $hidden = ['pivot'];

protected $casts = [
'mfa_enforced' => 'boolean',
];

/**
* The roles that belong to the role.
*/
Expand Down
2 changes: 1 addition & 1 deletion app/Auth/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
protected $hidden = [
'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id',
'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id', 'pivot',
];

/**
Expand Down
11 changes: 8 additions & 3 deletions app/Http/Controllers/Api/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ protected function apiListingResponse(Builder $query, array $fields, array $modi
*/
public function getValidationRules(): array
{
if (method_exists($this, 'rules')) {
return $this->rules();
}
return $this->rules();
}

/**
* Get the validation rules for the actions in this controller.
* Defaults to a $rules property but can be a rules() method.
*/
protected function rules(): array
{
return $this->rules;
}
}
136 changes: 136 additions & 0 deletions app/Http/Controllers/Api/RoleApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

namespace BookStack\Http\Controllers\Api;

use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Auth\Role;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class RoleApiController extends ApiController
{
protected PermissionsRepo $permissionsRepo;

protected array $fieldsToExpose = [
'display_name', 'description', 'mfa_enforced', 'external_auth_id', 'created_at', 'updated_at',
];

protected $rules = [
'create' => [
'display_name' => ['required', 'string', 'min:3', 'max:180'],
'description' => ['string', 'max:180'],
'mfa_enforced' => ['boolean'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'permissions.*' => ['string'],
],
'update' => [
'display_name' => ['string', 'min:3', 'max:180'],
'description' => ['string', 'max:180'],
'mfa_enforced' => ['boolean'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'permissions.*' => ['string'],
]
];

public function __construct(PermissionsRepo $permissionsRepo)
{
$this->permissionsRepo = $permissionsRepo;

// Checks for all endpoints in this controller
$this->middleware(function ($request, $next) {
$this->checkPermission('user-roles-manage');

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

/**
* Get a listing of roles in the system.
* Requires permission to manage roles.
*/
public function list()
{
$roles = Role::query()->select(['*'])
->withCount(['users', 'permissions']);

return $this->apiListingResponse($roles, [
...$this->fieldsToExpose,
'permissions_count',
'users_count',
]);
}

/**
* Create a new role in the system.
* Permissions should be provided as an array of permission name strings.
* Requires permission to manage roles.
*/
public function create(Request $request)
{
$data = $this->validate($request, $this->rules()['create']);

$role = null;
DB::transaction(function () use ($data, &$role) {
$role = $this->permissionsRepo->saveNewRole($data);
});

$this->singleFormatter($role);

return response()->json($role);
}

/**
* View the details of a single role.
* Provides the permissions and a high-level list of the users assigned.
* Requires permission to manage roles.
*/
public function read(string $id)
{
$user = $this->permissionsRepo->getRoleById($id);
$this->singleFormatter($user);

return response()->json($user);
}

/**
* Update an existing role in the system.
* Permissions should be provided as an array of permission name strings.
* An empty "permissions" array would clear granted permissions.
* In many cases, where permissions are changed, you'll want to fetch the existing
* permissions and then modify before providing in your update request.
* Requires permission to manage roles.
*/
public function update(Request $request, string $id)
{
$data = $this->validate($request, $this->rules()['update']);
$role = $this->permissionsRepo->updateRole($id, $data);

$this->singleFormatter($role);

return response()->json($role);
}

/**
* Delete a role from the system.
* Requires permission to manage roles.
*/
public function delete(string $id)
{
$this->permissionsRepo->deleteRole(intval($id));

return response('', 204);
}

/**
* Format the given role model for single-result display.
*/
protected function singleFormatter(Role $role)
{
$role->load('users:id,name,slug');
$role->unsetRelation('permissions');
$role->setAttribute('permissions', $role->permissions()->orderBy('name', 'asc')->pluck('name'));
$role->makeVisible(['users', 'permissions']);
}
}
4 changes: 2 additions & 2 deletions app/Http/Controllers/Api/UserApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

class UserApiController extends ApiController
{
protected $userRepo;
protected UserRepo $userRepo;

protected $fieldsToExpose = [
protected array $fieldsToExpose = [
'email', 'created_at', 'updated_at', 'last_activity_at', 'external_auth_id',
];

Expand Down
26 changes: 15 additions & 11 deletions app/Http/Controllers/RoleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,17 @@ public function create(Request $request)
public function store(Request $request)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
$data = $this->validate($request, [
'display_name' => ['required', 'min:3', 'max:180'],
'description' => ['max:180'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'mfa_enforced' => ['string'],
]);

$this->permissionsRepo->saveNewRole($request->all());
$this->showSuccessNotification(trans('settings.role_create_success'));
$data['permissions'] = array_keys($data['permissions'] ?? []);
$data['mfa_enforced'] = ($data['mfa_enforced'] ?? 'false') === 'true';
$this->permissionsRepo->saveNewRole($data);

return redirect('/settings/roles');
}
Expand All @@ -100,19 +104,21 @@ public function edit(string $id)

/**
* Updates a user role.
*
* @throws ValidationException
*/
public function update(Request $request, string $id)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
$data = $this->validate($request, [
'display_name' => ['required', 'min:3', 'max:180'],
'description' => ['max:180'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'mfa_enforced' => ['string'],
]);

$this->permissionsRepo->updateRole($id, $request->all());
$this->showSuccessNotification(trans('settings.role_update_success'));
$data['permissions'] = array_keys($data['permissions'] ?? []);
$data['mfa_enforced'] = ($data['mfa_enforced'] ?? 'false') === 'true';
$this->permissionsRepo->updateRole($id, $data);

return redirect('/settings/roles');
}
Expand Down Expand Up @@ -145,15 +151,13 @@ public function delete(Request $request, string $id)
$this->checkPermission('user-roles-manage');

try {
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id', 0));
} catch (PermissionsException $e) {
$this->showErrorNotification($e->getMessage());

return redirect()->back();
}

$this->showSuccessNotification(trans('settings.role_delete_success'));

return redirect('/settings/roles');
}
}
Loading