Comment adapter UX / UI sous les autorisations

De nombreux projets ont des processus d'authentification (à un degré ou à un autre). De nombreuses meilleures pratiques ont été écrites dans toutes les technologies connues, etc. etc.


Mais l'utilisateur a fait une connexion et? Après tout, il ne peut pas tout faire. Comment déterminer ce qu'il peut voir ou non. Quels boutons il a le droit d'appuyer, ce qui peut changer ce qu'il faut créer ou supprimer.


Dans cet article, je veux considérer une approche pour résoudre ces problèmes sur les applications Web.


image


Pour commencer, l'autorisation réelle / effective ne peut se produire que sur le serveur. Chez Front End, on ne peut qu'améliorer l'interface utilisateur (hmm, la langue russe est puissante, mais pas toujours ...), c'est aussi l'expérience utilisateur au-delà de l'UX. Nous pouvons masquer les boutons sur lesquels l'utilisateur n'a pas le droit de cliquer, ou l'empêcher d'accéder à la page, ou afficher un message qu'il n'a pas le droit de telle ou telle action.


Et ici, la question se pose, comment le faire le plus correctement possible.


Commençons par définir le problème.


Nous avons créé l'application Todo et elle a différents types d'utilisateurs:


UTILISATEUR - peut voir toutes les tâches et les modifier (définir et supprimer V), mais ne peut pas supprimer ou créer et ne voit pas les statistiques.


ADMIN - peut voir toutes les tâches et en créer de nouvelles, mais ne voit pas les statistiques.


SUPER_ADMIN - peut voir toutes les tâches, créer de nouvelles et supprimer, peut également voir des statistiques.


afficher la tâcheCréer une tâchevérifier / décocher la tâche (mise à jour)supprimer la tâchevoir les statistiques
UTILISATEURVXVXX
AdminVVVXX
SUPER_ADMINVVVVV

Dans une telle situation, nous pouvons facilement nous débrouiller avec les rôles. Cependant, la situation peut changer beaucoup. Imaginez qu'il y ait un utilisateur qui doit avoir les mêmes droits qu'ADMIN et supprimer des tâches. Ou tout simplement USER avec la possibilité de voir les statistiques.


Une solution simple consiste à créer de nouveaux rôles.
Mais sur les grands systèmes, avec un nombre d'utilisateurs relativement important, on se perd rapidement dans un très grand nombre de rôles ... Et ici on rappelle les autorisations «droits utilisateurs». Pour une gestion plus facile, nous pouvons créer des groupes de plusieurs autorisations et les attacher à l'utilisateur. Il y a toujours la possibilité d'ajouter une autorisation spécifique à un utilisateur spécifique.


Des solutions similaires peuvent être trouvées dans de nombreux grands services. AWS, Google Cloud, SalesForce, etc.
De plus, des solutions similaires sont déjà implémentées dans de nombreux frameworks, par exemple Django (python).


Je veux donner un exemple d'implémentation pour les applications angulaires. (Sur l'exemple de la même application ToDo).


Vous devez d'abord déterminer toutes les autorisations possibles.


Divisé en fonctionnalités


  1. Nous avons des tâches et des statistiques.
  2. Nous déterminons les actions possibles avec chacun d'eux.
    • Tâche: créer, lire, mettre à jour, supprimer
    • Statistiques: lire (dans notre exemple, afficher uniquement)
  3. Créer une carte (MAP) des rôles et autorisations

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

À mon avis, c'est la meilleure option, mais dans la plupart des cas, le serveur retournera quelque chose comme ceci:


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

Moins lisible, mais aussi très bon.


Voici à quoi ressemble notre application si l'utilisateur dispose de toutes les autorisations possibles.


image


Commençons par USER, voici à quoi ressemblent ses autorisations:


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

1) L'UTILISATEUR ne peut pas voir les statistiques, c'est-à-dire qu'il ne peut en principe pas accéder à la page des statistiques.


Pour de telles situations, Angular a des gardes, qui sont enregistrés au niveau Routes ( documentation ).


Dans notre cas, cela ressemble à ceci:


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

Vous devez faire attention à l'objet dans les données.


peremissions = 'stats', ce sont les permissions dont l'utilisateur doit disposer pour avoir accès à cette page.


En fonction des données.permissions requises et des autorisations que le serveur PermissionsGuardService nous a accordées, il décidera de laisser ou non USER accéder à la 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') } } 

Cette fonction contient la logique de décision - existe-t-il une autorisation irremplaçable (obligatoire) parmi toutes les autorisations de l'UTILISATEUR (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


Et ainsi. Notre UTILISATEUR ne peut pas accéder à la page des statistiques. Cependant, il peut toujours créer des tâches et les supprimer.


Pour que l'UTILISATEUR ne puisse pas supprimer la tâche, il suffira de supprimer le rouge (X) de la ligne de tâche.
À cette fin, nous utiliserons la directive structurelle .


 <!-- 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


L'UTILISATEUR ne voit plus le bouton SUPPRIMER mais peut toujours ajouter de nouvelles tâches.


Pour supprimer un champ de saisie - gâchera tout type de notre application. La solution correcte dans cette situation consiste à disable les champs de saisie.


Nous utiliserons des tuyaux.


 <!-- 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') } } 

Et maintenant, USER ne peut voir que les tâches et les modifier (V / X). Cependant, nous avons encore un bouton appelé «Effacement terminé».


Supposons que nous ayons les exigences suivantes du chef de produit:


  1. Le bouton «Effacer terminé» doit être visible par tous et toujours.
  2. Il doit également être cliquable.
  3. Si USER sans les autorisations correspondantes appuie sur le bouton, un message devrait apparaître.

La directive de construction ne nous aidera pas, ainsi que les tuyaux.
Il n'est pas non plus très pratique d'écrire des autorisations en vérifiant les fonctions des composants.


Tout ce dont nous avons besoin est d'effectuer une vérification des autorisations entre le clic et l'exécution de la fonction liée.


À mon avis, cela vaut la peine d'utiliser des décorateurs.


 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(); } } 

Les orateurs méritent un article séparé.


Notre résultat final:


image


Le résultat:


Cette approche nous permet d'adapter facilement et dynamiquement notre UX en fonction des autorisations dont dispose l'utilisateur.


Et surtout, pour cela, nous n'avons pas besoin de pousser les services dans tous les composants ou de marteler dans les modèles '* ngIf'.


La demande est complète .


Je me ferai un plaisir de commenter.

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


All Articles