Better permission system

This commit is contained in:
Matteo Gheza 2024-01-07 18:43:52 +01:00
parent dbc50da95a
commit 676ae0a0e9
22 changed files with 158 additions and 65 deletions

View File

@ -17,6 +17,7 @@ class AlertController extends Controller
*/
public function index()
{
if(!request()->user()->hasPermission("alerts-read")) abort(401);
return response()->json(
request()->query('full', false) ?
Alert::with(['crew.user' => function($query) {
@ -47,6 +48,7 @@ class AlertController extends Controller
*/
public function store(Request $request)
{
if(!$request->user()->hasPermission("alerts-create")) abort(401);
$alert = Alerts::addAlert(
$request->input('type', 'support'),
$request->input('ignoreWarning', false)
@ -64,6 +66,7 @@ class AlertController extends Controller
*/
public function show(Request $request, $id)
{
if(!$request->user()->hasPermission("alerts-read")) abort(401);
return response()->json(
Alert::where('id', $id)
->with(['crew.user' => function($query) {
@ -88,8 +91,7 @@ class AlertController extends Controller
*/
public function update(Request $request, $id)
{
//TODO: improve permissions and roles
if(!$request->user()->hasPermission("users-read")) abort(401);
if(!$request->user()->hasPermission("alerts-update")) abort(401);
$alert = Alert::find($id);
$alert->notes = $request->input('notes', $alert->notes);
$alert->closed = $request->input('closed', $alert->closed);
@ -116,6 +118,7 @@ class AlertController extends Controller
*/
public function setResponse(Request $request, $id)
{
if(!$request->user()->hasPermission("alerts-read")) abort(401);
try {
Alerts::updateAlertResponse($id, $request->input('response'));
} catch(AlertClosed $e) {

View File

@ -13,6 +13,7 @@ class AuthController extends Controller
{
public function register(Request $request)
{
if(!$request->user()->hasPermission("users-create")) abort(401);
$validatedData = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',

View File

@ -38,7 +38,6 @@ class DocumentsController extends Controller
public function serveDrivingLicenseScan($uuid)
{
//TODO: check if the user has access to the document
$document = DocumentFile::where('uuid', $uuid)->firstOrFail();
return response()->file(storage_path('app/public/' . $document->file_path));

View File

@ -17,6 +17,7 @@ class ServiceController extends Controller
*/
public function index(Request $request)
{
if(!$request->user()->hasPermission("services-read")) abort(401);
User::where('id', $request->user()->id)->update(['last_access' => now()]);
$query = Service::join('users', 'users.id', '=', 'chief_id')
@ -48,6 +49,7 @@ class ServiceController extends Controller
*/
public function show(Request $request, $id)
{
if(!$request->user()->hasPermission("services-read")) abort(401);
User::where('id', $request->user()->id)->update(['last_access' => now()]);
return response()->json(
@ -80,6 +82,9 @@ class ServiceController extends Controller
{
$adding = !isset($request->id) || is_null($request->id);
if(!$adding && !$request->user()->hasPermission("services-update")) abort(401);
if($adding && !$request->user()->hasPermission("services-create")) abort(401);
$service = $adding ? new Service() : Service::where("id",$request->id)->with('drivers')->with('crew')->first();
if(is_null($service)) abort(404);
@ -155,8 +160,9 @@ class ServiceController extends Controller
/**
* Remove the specified resource from storage.
*/
public function destroy($id)
public function destroy(Request $request, $id)
{
if(!$request->user()->hasPermission("services-delete")) abort(401);
$service = Service::find($id);
$usersToDecrement = $this->extractServiceUsers($service);
User::whereIn('id', $usersToDecrement)->decrement('services');

View File

@ -14,6 +14,7 @@ class ServiceTypeController extends Controller
*/
public function index(Request $request)
{
if(!$request->user()->hasPermission("services-read")) abort(401);
User::where('id', $request->user()->id)->update(['last_access' => now()]);
return response()->json(
@ -26,6 +27,7 @@ class ServiceTypeController extends Controller
*/
public function create(Request $request)
{
if(!$request->user()->hasPermission("services-create")) abort(401);
$serviceType = new ServiceType();
$serviceType->name = $request->name;
$serviceType->save();

View File

@ -14,6 +14,7 @@ class StatsController extends Controller
*/
public function services(Request $request)
{
if(!$request->user()->hasPermission("services-read")) abort(401);
$query = Service::select('id','code','chief_id','type_id','place_id','notes','start','end','added_by_id','created_at')
->with('place')
->with('drivers:id')

View File

@ -15,6 +15,7 @@ class TrainingController extends Controller
*/
public function index(Request $request)
{
if(!$request->user()->hasPermission("trainings-read")) abort(401);
User::where('id', $request->user()->id)->update(['last_access' => now()]);
$query = Training::join('users', 'users.id', '=', 'chief_id')
@ -44,6 +45,7 @@ class TrainingController extends Controller
*/
public function show(Request $request, $id)
{
if(!$request->user()->hasPermission("trainings-read")) abort(401);
User::where('id', $request->user()->id)->update(['last_access' => now()]);
return response()->json(
@ -70,6 +72,9 @@ class TrainingController extends Controller
{
$adding = !isset($request->id) || is_null($request->id);
if(!$adding && !$request->user()->hasPermission("trainings-update")) abort(401);
if($adding && !$request->user()->hasPermission("trainings-create")) abort(401);
$training = $adding ? new Training() : Training::where("id",$request->id)->with('crew')->first();
if(is_null($training)) abort(404);
@ -107,8 +112,9 @@ class TrainingController extends Controller
/**
* Remove the specified resource from storage.
*/
public function destroy($id)
public function destroy(Request $request, $id)
{
if(!$request->user()->hasPermission("trainings-delete")) abort(401);
$training = Training::find($id);
$usersToDecrement = $this->extractTrainingUsers($training);
User::whereIn('id', $usersToDecrement)->decrement('trainings');

View File

@ -16,6 +16,7 @@ class UserController extends Controller
*/
public function index(Request $request)
{
if(!$request->user()->hasPermission("users-read")) abort(401);
$requestedCols = ['id', 'chief', 'last_access', 'name', 'available', 'driver', 'services', 'availability_minutes'];
if($request->user()->isAbleTo("users-read")) $requestedCols[] = "phone_number";
@ -58,6 +59,8 @@ class UserController extends Controller
*/
public function show(Request $request, User $user)
{
if($request->user()->id != $user->id && !$request->user()->hasPermission("users-read")) abort(401);
User::where('id', $request->user()->id)->update(['last_access' => now()]);
//TODO: do not display useless or hidden info to the user about documentFile (like filePath and id) but only the url (see below)
@ -80,6 +83,9 @@ class UserController extends Controller
*/
public function update(Request $request, User $user)
{
if($request->user()->id != $user->id && !$request->user()->hasPermission("users-update")) abort(401);
if($request->user()->id == $user->id && !$request->user()->hasPermission("user-update")) abort(401);
$request->validate([
'name' => 'required|string|max:255',
'surname' => 'string|max:255',
@ -106,14 +112,22 @@ class UserController extends Controller
'boot_size' => 'string|max:255'
]);
/*
//TODO: new user permissions
if($request->user()->isAbleTo("users-update")) {
$user->update($request->all());
} else {
$user->update($request->except(['chief', 'driver', 'banned', 'hidden']));
}
*/
$canSetChief = $request->user()->id == $user->id ?
$request->user()->hasPermission("user-set-chief") :
$request->user()->hasPermission("users-set-chief");
$canSetDriver = $request->user()->id == $user->id ?
$request->user()->hasPermission("user-set-driver") :
$request->user()->hasPermission("users-set-driver");
$canBan = $request->user()->id == $user->id ?
$request->user()->hasPermission("user-ban") :
$request->user()->hasPermission("users-ban");
$canHide = $request->user()->id == $user->id ?
$request->user()->hasPermission("user-hide") :
$request->user()->hasPermission("users-hide");
if(!$canSetChief) $request->request->remove('chief');
if(!$canSetDriver) $request->request->remove('driver');
if(!$canBan) $request->request->remove('banned');
if(!$canHide) $request->request->remove('hidden');
$user->update($request->all());

View File

@ -13,25 +13,45 @@ return [
'roles_structure' => [
'superadmin' => [
'users' => 'c,r,u,d,i',
'users' => 'c,r,u,d,i,b,h,sc,sd',
'user' => 'u,h,sc,sd',
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'c,r,u',
],
'admin' => [
'users' => 'c,r,u'
'users' => 'c,r,u,d,i,b,h,sc,sd',
'user' => 'u,h,sc,sd',
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'c,r,u',
],
'chief' => [
'users' => 'r'
'users' => 'r,u,sc,sd',
'user' => 'u',
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'c,r,u',
],
'user' => [
'users' => 'lr'
'users' => 'lr',
'user' => 'u',
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'r',
]
],
'permissions_map' => [
'c' => 'create',
'lr' => 'limitedRead',
'lr' => 'limited-read',
'r' => 'read',
'u' => 'update',
'd' => 'delete',
'i' => 'impersonate'
'i' => 'impersonate',
'b' => 'ban',
'h' => 'hide',
'sc' => 'set-chief',
'sd' => 'set-driver',
]
];

View File

@ -59,11 +59,11 @@
<div class="well well-lg card card-block card-header">
<label for="details" class="form-label" translate>alert.details</label>
<textarea class="form-control" id="details" rows="3"
[(ngModel)]="notes" [disabled]="!auth.profile.can('users-read') || alertClosed==1" (keyup)="notesUpdated()"></textarea>
<button class="btn btn-secondary mt-2" (click)="saveAlertSettings()" [disabled]="!auth.profile.can('users-read') || !notesHasUnsavedChanges || alertClosed" translate>save_changes</button>
[(ngModel)]="notes" [disabled]="!auth.profile.can('alerts-update') || alertClosed==1" (keyup)="notesUpdated()"></textarea>
<button class="btn btn-secondary mt-2" (click)="saveAlertSettings()" [disabled]="!auth.profile.can('alerts-update') || !notesHasUnsavedChanges || alertClosed" translate>save_changes</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" (click)="deleteAlert()" *ngIf="auth.profile.can('users-read') && !alertClosed">{{ 'alert.delete'|translate }} <i class="fas fa-exclamation-triangle"></i></button>
<button type="button" class="btn btn-danger" (click)="deleteAlert()" *ngIf="auth.profile.can('alerts-update') && !alertClosed">{{ 'alert.delete'|translate }} <i class="fas fa-exclamation-triangle"></i></button>
<button type="button" class="btn btn-secondary" (click)="bsModalRef.hide()">{{ 'close'|translate }}</button>
</div>

View File

@ -76,7 +76,7 @@ export class ModalAlertComponent implements OnInit, OnDestroy {
}
saveAlertSettings() {
if(!this.auth.profile.can('users-read')) return;
if(!this.auth.profile.can('alerts-update')) return;
this.api.patch(`alerts/${this.id}`, {
notes: this.notes
}).then((response) => {
@ -91,7 +91,7 @@ export class ModalAlertComponent implements OnInit, OnDestroy {
}
deleteAlert() {
if(!this.auth.profile.can('users-read')) return;
if(!this.auth.profile.can('alerts-update')) return;
this.translate.get([
'alert.delete_confirm_title',
'alert.delete_confirm_text',
@ -135,7 +135,7 @@ export class ModalAlertComponent implements OnInit, OnDestroy {
}
setCurrentUserResponse(response: number) {
if(!this.auth.profile.can('users-read')) return;
if(!this.auth.profile.can('alerts-read')) return;
this.api.post(`alerts/${this.id}/response`, {
response
}).then((response) => {

View File

@ -32,7 +32,7 @@ export class ModalUserInfoComponent implements OnInit {
}).catch((err) => {
console.log(err);
});
this.canGoToEditPage = this.auth.profile.id === this.id || this.auth.profile.can('users-read');
this.canGoToEditPage = this.auth.profile.id === this.id || this.auth.profile.can('users-update');
}
goToEditPage() {

View File

@ -17,12 +17,13 @@
<th>{{ 'available'|translate|titlecase }}</th>
<th>{{ 'driver'|translate|titlecase }}</th>
<ng-container *ngIf="auth.profile.can('users-read')">
<th>{{ 'call'|translate|titlecase }}</th>
<th>{{ 'call'|translate|titlecase }}</th>
</ng-container>
<th>{{ 'services'|translate|titlecase }}</th>
<th>{{ 'availability_minutes'|translate|titlecase }}</th>
<th>{{ 'edit'|translate|titlecase }}</th>
<!-- TODO: hide if user can't access -->
<ng-container *ngIf="auth.profile.can('users-update')">
<th>{{ 'edit'|translate|titlecase }}</th>
</ng-container>
</tr>
</thead>
<tbody id="table_body">
@ -50,8 +51,7 @@
</td>
<td>{{ row.services }}</td>
<td>{{ row.availability_minutes }}</td>
<td (click)="editUser(row.id)"><i class="fa fa-edit"></i></td>
<!-- TODO: hide if user can't access -->
<td *ngIf="auth.profile.can('users-update')" (click)="editUser(row.id)"><i class="fa fa-edit"></i></td>
</tr>
</tbody>
</table>
@ -91,8 +91,8 @@
<th>{{ 'place'|translate|titlecase }}</th>
<th>{{ 'notes'|translate|titlecase }}</th>
<th>{{ 'type'|translate|titlecase }}</th>
<th>{{ 'update'|translate|titlecase }}</th>
<th>{{ 'remove'|translate|titlecase }}</th>
<th *ngIf="auth.profile.can('services-update')">{{ 'update'|translate|titlecase }}</th>
<th *ngIf="auth.profile.can('services-delete')">{{ 'remove'|translate|titlecase }}</th>
</tr>
</thead>
<tbody id="table_body">
@ -112,8 +112,8 @@
</td>
<td>{{ row.notes }}</td>
<td>{{ row.type }}</td>
<td (click)="editService(row.id)"><i class="fa fa-edit"></i></td>
<td (click)="deleteService(row.id)"><i class="fa fa-trash"></i></td>
<td (click)="editService(row.id)" *ngIf="auth.profile.can('services-update')"><i class="fa fa-edit"></i></td>
<td (click)="deleteService(row.id)" *ngIf="auth.profile.can('services-delete')"><i class="fa fa-trash"></i></td>
</tr>
</tbody>
</table>
@ -131,8 +131,8 @@
<th>{{ 'crew'|translate|titlecase }}</th>
<th>{{ 'place'|translate|titlecase }}</th>
<th>{{ 'notes'|translate|titlecase }}</th>
<th>{{ 'update'|translate|titlecase }}</th>
<th>{{ 'remove'|translate|titlecase }}</th>
<th *ngIf="auth.profile.can('trainings-update')">{{ 'update'|translate|titlecase }}</th>
<th *ngIf="auth.profile.can('trainings-delete')">{{ 'remove'|translate|titlecase }}</th>
</tr>
</thead>
<tbody id="table_body">
@ -145,8 +145,8 @@
<td>{{ extractNamesFromObject(row.crew).join(', ') }}</td>
<td>{{ row.place }}</td>
<td>{{ row.notes }}</td>
<td (click)="editTraining(row.id)"><i class="fa fa-edit"></i></td>
<td (click)="deleteTraining(row.id)"><i class="fa fa-trash"></i></td>
<td (click)="editTraining(row.id)" *ngIf="auth.profile.can('trainings-update')"><i class="fa fa-edit"></i></td>
<td (click)="deleteTraining(row.id)" *ngIf="auth.profile.can('trainings-delete')"><i class="fa fa-trash"></i></td>
</tr>
</tbody>
</table>

View File

@ -201,7 +201,9 @@ export class TableComponent implements OnInit, OnDestroy {
}
onMoreDetails(rowId: number) {
this.moreDetails.emit({rowId});
if(this.auth.profile.can('users-update')) {
this.moreDetails.emit({rowId});
}
}
editUser(id: number) {

View File

@ -3,6 +3,7 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@a
import { Observable } from 'rxjs';
import { GuardLoaderIconService } from '../_services/guard-loader-icon.service';
import { AuthService } from '../_services/auth.service';
import Swal from 'sweetalert2';
@Injectable({
providedIn: 'root'
@ -24,6 +25,20 @@ export class AuthorizeGuard {
this.router.navigate(['login', state.url.replace('/', '')]);
return false;
} else {
console.log(route.data);
if(route.data["permissionsRequired"]) {
let permissionsRequired = route.data["permissionsRequired"];
console.log(permissionsRequired, this.authService.profile.permissions);
if(!permissionsRequired.every((permission: string) => this.authService.profile.permissions.includes(permission))) {
//TODO: translate
Swal.fire({
title: "Non hai i permessi necessari per accedere a questa pagina",
icon: "error",
confirmButtonText: "Ok"
});
return false;
}
}
return true;
}
}

View File

@ -115,12 +115,23 @@ export class EditUserComponent implements OnInit {
}
ngOnInit(): void {
if(!this.auth.profile.can('users-ban') || this.id === this.auth.profile.id) {
this.profileForm.get('banned')?.disable();
}
if(!this.auth.profile.can('users-hide') || (!this.auth.profile.can('user-hide') && this.id === this.auth.profile.id)) {
this.profileForm.get('hidden')?.disable();
}
let canSetChief = this.id == this.auth.profile.id ?
this.auth.profile.can('user-set-chief') :
this.auth.profile.can('users-set-chief');
let canSetDriver = this.id == this.auth.profile.id ?
this.auth.profile.can('user-set-driver') :
this.auth.profile.can('users-set-driver');
let canBan = this.id == this.auth.profile.id ?
this.auth.profile.can('user-ban') :
this.auth.profile.can('users-ban');
let canHide = this.id == this.auth.profile.id ?
this.auth.profile.can('user-hide') :
this.auth.profile.can('users-hide');
if(!canSetChief) this.profileForm.get('chief')?.disable();
if(!canSetDriver) this.profileForm.get('driver')?.disable();
if(!canBan) this.profileForm.get('banned')?.disable();
if(!canHide) this.profileForm.get('hidden')?.disable();
}
onDrivingLicenseScanSelected(event: any) {

View File

@ -18,7 +18,7 @@
</button>
</div>
<owner-image></owner-image>
<div class="text-center" *ngIf="auth.profile.can('users-read')">
<div class="text-center" *ngIf="auth.profile.can('alerts-create')">
<div class="btn-group" role="group">
<button type="button" class="btn btn-danger" (click)="addAlert('full')" [disabled]="!api.availableUsers || api.availableUsers! < 5 || alertLoading">
🚒 Richiedi squadra completa

View File

@ -91,7 +91,7 @@ export class ListComponent implements OnInit, OnDestroy {
addAlert(type: string, ignoreWarning = false) {
this.alertLoading = true;
if(!this.auth.profile.can('users-read')) return;
if(!this.auth.profile.can('alerts-create')) return;
this.api.post("alerts", {
type,
ignoreWarning

View File

@ -33,6 +33,8 @@ export class AuthService {
return this.profile.permissions.includes(permission);
}
this.profile.profilePageLink = "/users/" + this.profile.id;
Sentry.setUser({
id: this.profile.id,
name: this.profile.name

View File

@ -17,7 +17,7 @@ const routes: Routes = [
canActivate: [AuthorizeGuard]
},
{ path: 'logs', component: LogsComponent, canActivate: [AuthorizeGuard] },
{ path: 'services', component: ServicesComponent, canActivate: [AuthorizeGuard] },
{ path: 'services', component: ServicesComponent, canActivate: [AuthorizeGuard], data: {permissionsRequired: ['services-read']} },
{
path: 'place-details',
loadChildren: () => import('./_routes/place-details/place-details.module').then(m => m.PlaceDetailsModule),
@ -26,13 +26,15 @@ const routes: Routes = [
{
path: 'services/:id',
loadChildren: () => import('./_routes/edit-service/edit-service.module').then(m => m.EditServiceModule),
canActivate: [AuthorizeGuard]
canActivate: [AuthorizeGuard],
data: {permissionsRequired: ['services-read', 'services-update']}
},
{ path: 'trainings', component: TrainingsComponent, canActivate: [AuthorizeGuard] },
{ path: 'trainings', component: TrainingsComponent, canActivate: [AuthorizeGuard], data: {permissionsRequired: ['trainings-read']} },
{
path: 'trainings/:id',
loadChildren: () => import('./_routes/edit-training/edit-training.module').then(m => m.EditTrainingModule),
canActivate: [AuthorizeGuard]
canActivate: [AuthorizeGuard],
data: {permissionsRequired: ['trainings-read', 'trainings-update']}
},
{
path: 'stats',

View File

@ -3,7 +3,7 @@
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/services" translate>menu.services</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/trainings" translate>menu.trainings</a>
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/logs" translate>menu.logs</a>
<a style="float: right;" id="logout">{{ 'menu.hi'|translate|titlecase }}, {{ auth.profile.name }}. <b id="logout-text" (click)="auth.logout()" translate *ngIf="!auth.profile.impersonating_user">menu.logout</b><b id="logout-text" (click)="auth.logout()" translate *ngIf="auth.profile.impersonating_user">menu.stop_impersonating</b></a>
<a style="float: right;" id="logout" routerLinkActive="active" [routerLink]="auth.profile.profilePageLink">{{ 'menu.hi'|translate|titlecase }}, {{ auth.profile.name }}. <b id="logout-text" (click)="logout($event)" translate *ngIf="!auth.profile.impersonating_user">menu.logout</b><b id="logout-text" (click)="auth.logout()" translate *ngIf="auth.profile.impersonating_user">menu.stop_impersonating</b></a>
<a class="icon" id="menuButton" (click)="menuButtonClicked = !menuButtonClicked"></a>
</div>

View File

@ -57,22 +57,31 @@ export class AppComponent {
}
});
this.loadAlertsInterval = setInterval(() => {
console.log("Refreshing alerts...");
if(this.auth.profile.can("alerts-read")) {
this.loadAlertsInterval = setInterval(() => {
console.log("Refreshing alerts...");
this.loadAlerts();
}, 30000);
this.loadAlerts();
}, 30000);
this.loadAlerts();
this.api.alertsChanged.subscribe(() => {
this.loadAlerts();
});
this.api.alertsChanged.subscribe(() => {
this.loadAlerts();
});
}
}
openAlert(id: number) {
this.modalService.show(ModalAlertComponent, {
initialState: {
id: id
}
});
if(this.auth.profile.can("alerts-read")) {
this.modalService.show(ModalAlertComponent, {
initialState: {
id: id
}
});
}
}
logout(event: Event) {
event.preventDefault();
this.auth.logout();
}
}