📜 ⬆️ ⬇️

How to adapt UX / UI for permissions

In many projects, there are authentication processes (to one degree or another). Many “best practices” are written in all known technologies, etc. etc.


But here the user made login and? After all, he can not do anything. How to determine what he can see and what is not. What buttons has the right to click, what can change, what to create or delete.


In this article I want to consider the approach to solving these problems on the web application.


image


To begin with, the real / effective authorization can only occur on the server. At the Front End, we can only improve the user interface (hmm, the Russian language is powerful, but not always ...), it’s also the User Experience beyond UX. We can hide the buttons on which the user does not have the right to click, or not allow it to the page, or show a message that he does not have rights to this or that action.


And here the question arises how to do it as correctly as possible.


Let's start by defining the problem.


We created the Todo App and it has different types of users:


USER - can see all the tasks and change them (set and remove V), but can not delete or create and does not see the statistics.


ADMIN - can see all the tasks and create new ones, but does not see the statistics.


SUPER_ADMIN - can see all tasks, create new ones and delete, also can see statistics.


view taskCreate taskcheck / uncheck task (update)delete taskview stats
USERVXVXX
ADMINVVVXX
SUPER_ADMINVVVVV

In such a situation, we can easily get away with the roles of “roles”. However, the situation can change a lot. Imagine that there is a user who must have the same rights as ADMIN plus deleting tasks. Or simply USER with the ability to see statistics.


A simple solution is to create new roles.
But on large systems, with a relatively large number of users, we will quickly get lost in a huge number of roles ... And here we will recall the “user rights” permissions . For easier management, we can create groups of several permissions and attach them to the user. It is always possible to add specific permission to a specific user.


Such solutions can be found in many large services. AWS, Google Cloud, SalesForce, etc.
Similar solutions have already been implemented in many frameworks, for example Django (python).


I want to give an example of implementation for Angular applications. (On the example of the same ToDo App).


First you need to define all possible permissions.


Let's divide into features


  1. We have task and statistics.
  2. We define the possible actions with each of them.
    • Task: create, read, update, delete
    • Statistics: read (in our example, only viewing)
  3. Create a map (MAP) of roles and permissions

export const permissionsMap = { todos:{ create:'*', read:'*', update:'*', delete:'*' }, stats:'*' } 

In my opinion this is the best option, but in most cases the server will return something like this:


 export permissions = [ 'todos_create', 'todos_read', 'todos_update', 'todos_delete', 'stas' ] 

Less readable, but also very good.


This is how our application looks like if the user has all possible permissions.


image


Let's start with USER, so it looks like permissions:


 export const USERpermissionsMap = { todos:{ read:'*', update:'*', } } 

1) USER cannot see the statistics, that is, it basically cannot go over the statistics page.


For such situations in Angular there are Guards, which are written at the Routes level ( documentation ).


In our case, it looks like this:


 const routes: Routes = [ // {...Other routes}, { path: 'stats', component: TodoStatisticsComponent, canActivate: [ PermissionsGuardService ], data: { permission: 'stats' }, } ]; 

Pay attention to the object in the data.


peremissions = 'stats', this is exactly the permissions that the user needs to have in order to have access to this page.


Based on the required data.permissions and the permissions that the PermissionsGuardService server gave us will decide whether or not USER should be allowed to the page '/ stats'.


 @Injectable({ providedIn: 'root' }) export class PermissionsGuardService implements CanActivate { constructor( private store: Store<any>) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean { // Required permission to continue navigation const required = route.data.permission; // User permissions that we got from server const userPerms = this.getPermissions(); // verification const isPermitted = checkPermissions(required, userPerms); if (!isPermitted) { alert('ROUTE GUARD SAYS: \n You don\'t have permissions to see this page'); } return isPermitted; } getPermissions() { return localStorage.get('userPermissions') } } 

This function contains the logic of deciding whether there is a non-extrusive permission (required) among all permissions held by USER (userPerms).


 export function checkPermissions(required: string, userPerms) { // 1) Separate feature and action const [feature, action] = required.split('_'); // 2) Check if user have any type of access to the feature if (!userPerms.hasOwnProperty(feature)) { return false; } // 3) Check if user have permission for required action if (!userPerms[feature].hasOwnProperty(action)) { return false; } return true; } 

image


So. Our USER can not get to the statistics page. However, it can still create tasks and delete them.


In order for USER not to delete a task, it will be sufficient to remove the red (X) from the task string.
For this purpose, we use the Structural Directive .


 <!-- Similar to other Structural Directives in angular (*ngIF, *ngFor..) we have '*' in directive name Input for directive is a required permission to see this btn. --> <button *appPermissions="'todos_delete'" class="delete-button" (click)="removeSingleTodo()"> X </button> 

 @Directive({ selector: '[appPermissions]' }) export class PermissionsDirective { private _required: string; private _viewRef: EmbeddedViewRef<any> | null = null; private _templateRef: TemplateRef<any> | null = null; @Input() set appPermissions(permission: string) { this._required = permission; this._viewRef = null; this.init(); } constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) { this._templateRef = templateRef; } init() { const isPermitted = checkPermissions(this._required, this.getPermissions()); if (isPermitted) { this._viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef); } else { console.log('PERMISSIONS DIRECTIVE says \n You don\'t have permissions to see it'); } } getPermissions() { localStorage.get('userPermissions') } } 

image


Now USER does not see the DELETE button but can still add new tasks.


To remove the input field - will spoil the whole look of our application. The correct solution in this situation would be to disable the input field.


We use the pipe.


 <!-- as a value `permissions` pipe will get required permissions `permissions` pipe return true or false, that's why we have !('todos_create' | permissions) to set disable=true if pipe returns false --> <input class="centered-block" [disabled]="!('todos_create' | permissions)" placeholder="What needs to be done?" autofocus/> 

 @Pipe({ name: 'permissions' }) export class PermissionsPipe implements PipeTransform { constructor(private store: Store<any>) { } transform(required: any, args?: any): any { const isPermitted = checkPermissions(required, this.getPermissions()); if (isPermitted) { return true; } else { console.log('[PERMISSIONS PIPE] You don\'t have permissions'); return false; } } getPermissions() { return localStorage.get('userPermissions') } } 

And now the USER can only see the tasks and change them (V / X). However, we still have another button 'Clear completed'.


Suppose we have the following requirements of the Product Manager:


  1. The 'Clear Completed' button should be visible to everyone and always.
  2. It should also be clickable.
  3. In the event that USER without a corresponding perm press a button, a message should appear.

Constructional directive does not help us, nor does pipe.
Writing permissions to functions on permissions in a function is also not very convenient.


All we need is to perform permissions checking between the click and the execution of the bound function.


In my opinion it is worth taking advantage of decorators.


 export function Permissions(required) { return (classProto, propertyKey, descriptor) => { const originalFunction = descriptor.value; descriptor.value = function (...args: any[]) { const userPerms = localStorage.get('userPermissions') const isPermitted = checkPermissions(required, userPerms); if (isPermitted) { originalFunction.apply(this, args); } else { alert('you have no permissions \n [PERMISSIONS DECORATOR]'); } }; return descriptor; }; } 

 @Component({ selector: 'app-actions', templateUrl: './actions.component.html', styleUrls: ['./actions.component.css'] }) export class ActionsComponent implements OnInit { @Output() deleteCompleted = new EventEmitter(); constructor() { } @Permissions('todos_delete') public deleteCompleted() { this.deleteCompleted.emit(); } } 

Dorkoratory deserve a separate article.


Our final result:


image


Total:


This approach allows us to easily and dynamically adapt our UX in accordance with the permissions that the user has.


And most importantly, for this we don’t need to push services into all components or hammer in the templates '* ngIf'.


The entire application .


I would welcome comments.



Source: https://habr.com/ru/post/439100/