Angular - How to check authorization based on role and entity-states

Picture credits

What the heck is entity-state authorization?

Well, I guess there was nothing better in my head to name it while writing this post. However more or less what I am trying to say is related to the situations when you need to grant or un-grant to the current user the ability to apply specific action if the current entity state relies on X state and a different ability when the entity state relies on Y state and It came worst when all this should be on the same screen or even in the same component. Any libraries with role-base authorization will save you for this problem.
Tired of reading different articles with role-base page prevent solutions, I started thinking and coding without anything in mind yet, and suddenly it was there, I found simple, straightforward and very flexible solution to solve this kind of problems and its composed of four components.

  • One workflow-permissions map (JSON file)
  • A permissions service to actually do the authorization check
  • A directive to consume the check the authorization service

Step 1: get current user roles

Implement a service to retrieve from the server side (first time) or from session or cookies as you prefer for subsequent uses of it, the important thing here is to provide the user list of abilities (roles).

// Example
{
name: 'John Doe',
email: 'jd@doe.com',
roles: ['seller', 'seller_manager'], <-- this is what matters
accessToken: 'alotofletersrepresentinganaccesstokenhahahaaa!'
... more stuff
}
// imports here ...@Injectable()
export class CurrentUserService {
private userSubject = new ReplaySubject<User>(1);
private hasUser = false;

constructor(private usersApi: UserApi) {
}

public getUser(): Observable<User> {
if (!this.hasUser) {
this.fetchUser();
}
return this.userSubject.asObservable();
}

public fetchUser(): void {
this.usersApi.getCurrent() // <== http call to fetch userInfo
.subscribe(user => {
// user should contains roles has been granted
this.hasUser = true;
this.userSubject.next(user);
this.userSubject.complete();
}, (error) => {
this.hasUser = false;
this.userSubject.error(error);
});
}

}

Second step: Build your workflow & permissions map

This is nothing but the mapping of what can we do and who can we do by building a tree with the different entities and their own states. for example, let’s imagine the following sales process; Our application might have several role types. For our example let’s map the roles to SELLER, solutions ARCHITECT and CLIENT.

  • At this point, the CLIENT & SELLER can add the requirements to the opportunity so both can apply the action Add requirements when the requirements are placed then status of the opportunity might change to submitted
  • Once the requirements are placed the ARCHITECT might want to add a solution so he needs an action: Upload solution and probably the state might change to solved
  • Once the solution has been provided, the CLIENT might want to accept so he needs an action to approve solution and the state would change to solution_approved
{
"opportunity": {
"addOpportunity": { "permittedRoles": ["SELLER"] } },
"created": {
"addRequirement": {"permittedRoles": ["SELLER", "CLIENT"]}
},
"submitted": {
"addSolution": { "permittedRoles": ["ARCHITECT"]}
},
"solved": {
"approveSolution": { "permittedRoles": ["CLIENT"]}
}
}

Step 3: The check authorization service to consume the worflow & permissions map

Now that we have the process mapped into a workflow & permissions map, we need to create a service to consume it and check whether user is authorized or not and it could look like this:

// import statements here
// by the way in angular-cli we can put the JSON file in the
// enviroment.ts
@Injectable()
export class WorkflowEvents {
private readonly WORKFLOW_EVENTS = environment['workflow'];
private userRoles: Set<string>;
// do your remember the step 1 ? it is used here
constructor(private currentUserService: CurrentUserService) {
}
// returns a boolean observable
public checkAuthorization(path: any): Observable<boolean> {
// we are loading the roles only once
if (!this.userRoles) {
return this.currentUserService.getUser()
.map(currentUser => currentUser.roles)
.do(roles => {
const roles = roles.map(role => role.name);
this.userRoles = new Set(roles);
})
.map(roles => this.doCheckAuthorization(path));
}
return Observable.of(this.doCheckAuthorization(path));
}

private doCheckAuthorization(path: string[]): boolean {
if (path.length) {
const entry = this.findEntry(this.WORKFLOW_EVENTS, path);
if (entry && entry['permittedRoles']
&& this.userRoles.size) {
return entry.permittedRoles
.some(permittedRole => this.userRoles.has(permittedRole));
}
return false;
}
return false;
}
/**
* Recursively find workflow-map entry based on the path strings
*/
private findEntry(currentObject: any, keys: string[], index = 0) {
const key = keys[index];
if (currentObject[key] && index < keys.length - 1) {
return this.findEntry(currentObject[key], keys, index + 1);
} else if (currentObject[key] && index === keys.length - 1) {
return currentObject[key];
} else {
return false;
}
}

}

File 4: The directive

Once that we have the current user roles, a workflow permissions tree, and a service to check authorization for current user roles, now we need a way to put this alive, and the best way in angular 2/4 is a directive. Initially the directive I wrote was an attribute directive that was toggling the display CSS attribute but this could lead to performance issues because decedent components still being loading to DOM, so is better to use structural directives (Thanks to my colleague Petyo Cholakov for this good catch, see the difference here) since we can modify the DOM of the target element and it’s decedents so we can avoid the loading of unused elements.

@Directive({
selector: '[appCanAccess]'
})
export class CanAccessDirective implements OnInit, OnDestroy {
@Input('appCanAccess') appCanAccess: string | string[];
private permission$: Subscription;

constructor(private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private workflowEvents: WorkflowEvents) {
}

ngOnInit(): void {
this.applyPermission();
}

private applyPermission(): void {
this.permission$ = this.workflowEvents
.checkAuthorization(this.appCanAccess)
.subscribe(authorized => {
if (authorized) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
});
}

ngOnDestroy(): void {
this.permission$.unsubscribe();
}

}

Finally the resultant work

Now what we have all that we need is time to put them in action. so in our HTML template, the only thing we need to do is something like the following code

@Component({
selector: 'sp-pricing-panel',
template: `
<same-sample-component>
<button *appCanAccess="['opportunity', 'addOpportunity']">
Add Opportunity
</button>
<add-requirement-component *appCanAccess="['opportunity', opportunityObject.state, 'addRequirement']"></add-required-component><add-solution-component *appCanAccess="['opportunity', opportunityObject.state, 'addSolution']"></add-required-component><approve-solution-component *appCanAccess="['opportunity', opportunityObject.state, 'approveSolution']"></add-required-component></same-sample-component>`
})
export class SampleComponent implements OnInit {

@Input() opportunityObject: any;
constructor() {
}

ngOnInit() {
}
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store