Angular - How to check authorization based on role and entity-states
Nowadays is very common to find you in the state of building authentication and authorization in your application, digging on the web for authorization libraries and techniques is easy always to find solutions that only offer role-base authorization that prevents only to access a page, however almost often occurs that you need something else, entity-state authorization.
For the TL;DR here is the demo and the code used in the demo.
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.
- A service to provide the current user information (what really matter is a way to provide the user roles that current user belongs to or has been granted to play in the application)
- 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}
In my case more or less this is how it looks:
// 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.
Lest talk about the process:
- First the SELLER places a sales opportunity by executing the action Add new opportunity so the opportunity state probably is created
- 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
We are going to cut the process here otherwise this would grow too much and is not the case for this reading. So based on this process the mapping and assuming that the opportunity entity has a field that tracks the state, our workflow would look like this:
{
"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;
}
}
}
Basically what it does is to look a valid entry and check if the current user roles are included in the permittedRoles.
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
Let’s assume we have a sample component that includes the opportunity object:
@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() {
}
}
And that’s it, we can have a simple component presenting behavior depending on the user roles and the entity state.
Thanks to my colleagues Petyo and Govind for the catch and critics to my bad coding we could find this solution that can works perfectly to our needs, I hope this helps you too.
JUN 2018, small sample working => https://emsedano.github.io/ng-entity-state/