Skip to content

Commit c1167f8

Browse files
authored
Merge pull request #4051 from BookStackApp/roles_api
User Roles API Endpoint
2 parents fd45d28 + 4176b59 commit c1167f8

21 files changed

+580
-54
lines changed

app/Auth/Permissions/PermissionsRepo.php

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@
1212
class PermissionsRepo
1313
{
1414
protected JointPermissionBuilder $permissionBuilder;
15-
protected $systemRoles = ['admin', 'public'];
15+
protected array $systemRoles = ['admin', 'public'];
1616

17-
/**
18-
* PermissionsRepo constructor.
19-
*/
2017
public function __construct(JointPermissionBuilder $permissionBuilder)
2118
{
2219
$this->permissionBuilder = $permissionBuilder;
@@ -41,7 +38,7 @@ public function getAllRolesExcept(Role $role): Collection
4138
/**
4239
* Get a role via its ID.
4340
*/
44-
public function getRoleById($id): Role
41+
public function getRoleById(int $id): Role
4542
{
4643
return Role::query()->findOrFail($id);
4744
}
@@ -52,10 +49,10 @@ public function getRoleById($id): Role
5249
public function saveNewRole(array $roleData): Role
5350
{
5451
$role = new Role($roleData);
55-
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
52+
$role->mfa_enforced = boolval($roleData['mfa_enforced'] ?? false);
5653
$role->save();
5754

58-
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
55+
$permissions = $roleData['permissions'] ?? [];
5956
$this->assignRolePermissions($role, $permissions);
6057
$this->permissionBuilder->rebuildForRole($role);
6158

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

6764
/**
6865
* Updates an existing role.
69-
* Ensure Admin role always have core permissions.
66+
* Ensures Admin system role always have core permissions.
7067
*/
71-
public function updateRole($roleId, array $roleData)
68+
public function updateRole($roleId, array $roleData): Role
7269
{
7370
$role = $this->getRoleById($roleId);
7471

75-
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
76-
if ($role->system_name === 'admin') {
77-
$permissions = array_merge($permissions, [
78-
'users-manage',
79-
'user-roles-manage',
80-
'restrictions-manage-all',
81-
'restrictions-manage-own',
82-
'settings-manage',
83-
]);
72+
if (isset($roleData['permissions'])) {
73+
$this->assignRolePermissions($role, $roleData['permissions']);
8474
}
8575

86-
$this->assignRolePermissions($role, $permissions);
87-
8876
$role->fill($roleData);
89-
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
9077
$role->save();
9178
$this->permissionBuilder->rebuildForRole($role);
9279

9380
Activity::add(ActivityType::ROLE_UPDATE, $role);
81+
82+
return $role;
9483
}
9584

9685
/**
97-
* Assign a list of permission names to a role.
86+
* Assign a list of permission names to the given role.
9887
*/
99-
protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
88+
protected function assignRolePermissions(Role $role, array $permissionNameArray = []): void
10089
{
10190
$permissions = [];
10291
$permissionNameArray = array_values($permissionNameArray);
10392

104-
if ($permissionNameArray) {
93+
// Ensure the admin system role retains vital system permissions
94+
if ($role->system_name === 'admin') {
95+
$permissionNameArray = array_unique(array_merge($permissionNameArray, [
96+
'users-manage',
97+
'user-roles-manage',
98+
'restrictions-manage-all',
99+
'restrictions-manage-own',
100+
'settings-manage',
101+
]));
102+
}
103+
104+
if (!empty($permissionNameArray)) {
105105
$permissions = RolePermission::query()
106106
->whereIn('name', $permissionNameArray)
107107
->pluck('id')
@@ -114,13 +114,13 @@ protected function assignRolePermissions(Role $role, array $permissionNameArray
114114
/**
115115
* Delete a role from the system.
116116
* Check it's not an admin role or set as default before deleting.
117-
* If an migration Role ID is specified the users assign to the current role
117+
* If a migration Role ID is specified the users assign to the current role
118118
* will be added to the role of the specified id.
119119
*
120120
* @throws PermissionsException
121121
* @throws Exception
122122
*/
123-
public function deleteRole($roleId, $migrateRoleId)
123+
public function deleteRole(int $roleId, int $migrateRoleId = 0): void
124124
{
125125
$role = $this->getRoleById($roleId);
126126

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

134-
if ($migrateRoleId) {
134+
if ($migrateRoleId !== 0) {
135135
$newRole = Role::query()->find($migrateRoleId);
136136
if ($newRole) {
137137
$users = $role->users()->pluck('id')->toArray();

app/Auth/Permissions/RolePermission.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
/**
1010
* @property int $id
11+
* @property string $name
12+
* @property string $display_name
1113
*/
1214
class RolePermission extends Model
1315
{

app/Auth/Role.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,14 @@ class Role extends Model implements Loggable
2727
{
2828
use HasFactory;
2929

30-
protected $fillable = ['display_name', 'description', 'external_auth_id'];
30+
protected $fillable = ['display_name', 'description', 'external_auth_id', 'mfa_enforced'];
3131

3232
protected $hidden = ['pivot'];
3333

34+
protected $casts = [
35+
'mfa_enforced' => 'boolean',
36+
];
37+
3438
/**
3539
* The roles that belong to the role.
3640
*/

app/Auth/User.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
7272
*/
7373
protected $hidden = [
7474
'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
75-
'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id',
75+
'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id', 'pivot',
7676
];
7777

7878
/**

app/Http/Controllers/Api/ApiController.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,15 @@ protected function apiListingResponse(Builder $query, array $fields, array $modi
3232
*/
3333
public function getValidationRules(): array
3434
{
35-
if (method_exists($this, 'rules')) {
36-
return $this->rules();
37-
}
35+
return $this->rules();
36+
}
3837

38+
/**
39+
* Get the validation rules for the actions in this controller.
40+
* Defaults to a $rules property but can be a rules() method.
41+
*/
42+
protected function rules(): array
43+
{
3944
return $this->rules;
4045
}
4146
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
namespace BookStack\Http\Controllers\Api;
4+
5+
use BookStack\Auth\Permissions\PermissionsRepo;
6+
use BookStack\Auth\Role;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Support\Facades\DB;
9+
10+
class RoleApiController extends ApiController
11+
{
12+
protected PermissionsRepo $permissionsRepo;
13+
14+
protected array $fieldsToExpose = [
15+
'display_name', 'description', 'mfa_enforced', 'external_auth_id', 'created_at', 'updated_at',
16+
];
17+
18+
protected $rules = [
19+
'create' => [
20+
'display_name' => ['required', 'string', 'min:3', 'max:180'],
21+
'description' => ['string', 'max:180'],
22+
'mfa_enforced' => ['boolean'],
23+
'external_auth_id' => ['string'],
24+
'permissions' => ['array'],
25+
'permissions.*' => ['string'],
26+
],
27+
'update' => [
28+
'display_name' => ['string', 'min:3', 'max:180'],
29+
'description' => ['string', 'max:180'],
30+
'mfa_enforced' => ['boolean'],
31+
'external_auth_id' => ['string'],
32+
'permissions' => ['array'],
33+
'permissions.*' => ['string'],
34+
]
35+
];
36+
37+
public function __construct(PermissionsRepo $permissionsRepo)
38+
{
39+
$this->permissionsRepo = $permissionsRepo;
40+
41+
// Checks for all endpoints in this controller
42+
$this->middleware(function ($request, $next) {
43+
$this->checkPermission('user-roles-manage');
44+
45+
return $next($request);
46+
});
47+
}
48+
49+
/**
50+
* Get a listing of roles in the system.
51+
* Requires permission to manage roles.
52+
*/
53+
public function list()
54+
{
55+
$roles = Role::query()->select(['*'])
56+
->withCount(['users', 'permissions']);
57+
58+
return $this->apiListingResponse($roles, [
59+
...$this->fieldsToExpose,
60+
'permissions_count',
61+
'users_count',
62+
]);
63+
}
64+
65+
/**
66+
* Create a new role in the system.
67+
* Permissions should be provided as an array of permission name strings.
68+
* Requires permission to manage roles.
69+
*/
70+
public function create(Request $request)
71+
{
72+
$data = $this->validate($request, $this->rules()['create']);
73+
74+
$role = null;
75+
DB::transaction(function () use ($data, &$role) {
76+
$role = $this->permissionsRepo->saveNewRole($data);
77+
});
78+
79+
$this->singleFormatter($role);
80+
81+
return response()->json($role);
82+
}
83+
84+
/**
85+
* View the details of a single role.
86+
* Provides the permissions and a high-level list of the users assigned.
87+
* Requires permission to manage roles.
88+
*/
89+
public function read(string $id)
90+
{
91+
$user = $this->permissionsRepo->getRoleById($id);
92+
$this->singleFormatter($user);
93+
94+
return response()->json($user);
95+
}
96+
97+
/**
98+
* Update an existing role in the system.
99+
* Permissions should be provided as an array of permission name strings.
100+
* An empty "permissions" array would clear granted permissions.
101+
* In many cases, where permissions are changed, you'll want to fetch the existing
102+
* permissions and then modify before providing in your update request.
103+
* Requires permission to manage roles.
104+
*/
105+
public function update(Request $request, string $id)
106+
{
107+
$data = $this->validate($request, $this->rules()['update']);
108+
$role = $this->permissionsRepo->updateRole($id, $data);
109+
110+
$this->singleFormatter($role);
111+
112+
return response()->json($role);
113+
}
114+
115+
/**
116+
* Delete a role from the system.
117+
* Requires permission to manage roles.
118+
*/
119+
public function delete(string $id)
120+
{
121+
$this->permissionsRepo->deleteRole(intval($id));
122+
123+
return response('', 204);
124+
}
125+
126+
/**
127+
* Format the given role model for single-result display.
128+
*/
129+
protected function singleFormatter(Role $role)
130+
{
131+
$role->load('users:id,name,slug');
132+
$role->unsetRelation('permissions');
133+
$role->setAttribute('permissions', $role->permissions()->orderBy('name', 'asc')->pluck('name'));
134+
$role->makeVisible(['users', 'permissions']);
135+
}
136+
}

app/Http/Controllers/Api/UserApiController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313

1414
class UserApiController extends ApiController
1515
{
16-
protected $userRepo;
16+
protected UserRepo $userRepo;
1717

18-
protected $fieldsToExpose = [
18+
protected array $fieldsToExpose = [
1919
'email', 'created_at', 'updated_at', 'last_activity_at', 'external_auth_id',
2020
];
2121

app/Http/Controllers/RoleController.php

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,17 @@ public function create(Request $request)
7474
public function store(Request $request)
7575
{
7676
$this->checkPermission('user-roles-manage');
77-
$this->validate($request, [
77+
$data = $this->validate($request, [
7878
'display_name' => ['required', 'min:3', 'max:180'],
7979
'description' => ['max:180'],
80+
'external_auth_id' => ['string'],
81+
'permissions' => ['array'],
82+
'mfa_enforced' => ['string'],
8083
]);
8184

82-
$this->permissionsRepo->saveNewRole($request->all());
83-
$this->showSuccessNotification(trans('settings.role_create_success'));
85+
$data['permissions'] = array_keys($data['permissions'] ?? []);
86+
$data['mfa_enforced'] = ($data['mfa_enforced'] ?? 'false') === 'true';
87+
$this->permissionsRepo->saveNewRole($data);
8488

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

101105
/**
102106
* Updates a user role.
103-
*
104-
* @throws ValidationException
105107
*/
106108
public function update(Request $request, string $id)
107109
{
108110
$this->checkPermission('user-roles-manage');
109-
$this->validate($request, [
111+
$data = $this->validate($request, [
110112
'display_name' => ['required', 'min:3', 'max:180'],
111113
'description' => ['max:180'],
114+
'external_auth_id' => ['string'],
115+
'permissions' => ['array'],
116+
'mfa_enforced' => ['string'],
112117
]);
113118

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

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

147153
try {
148-
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
154+
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id', 0));
149155
} catch (PermissionsException $e) {
150156
$this->showErrorNotification($e->getMessage());
151157

152158
return redirect()->back();
153159
}
154160

155-
$this->showSuccessNotification(trans('settings.role_delete_success'));
156-
157161
return redirect('/settings/roles');
158162
}
159163
}

0 commit comments

Comments
 (0)