Returning Permissions With My API Response Payloads In ColdFusion
At InVision, I work on a large AngularJS SPA (Single-Page Application) that is backed by a monolithic ColdFusion API. When the SPA initially loads, it is provided with as JSON (JavaScript Object Notation) payload about the user that is then used to render various Calls-To-Action (CTA) within the View Partials. Lately, however, I've been leaning heavily into returning permissions information right inside my partial API responses. This is proving to reduce the complexity of my view-logic while also making the views more flexible and the application more responsive to changes.
With the older model of rendering, the SPA is bootstrapped with information about the user and their role within the contextual organization. This role data is then used to determine which buttons to show and which links to render. This approach certainly works, but it has a number of downsides:
A user may have the SPA open for days at a stretch. Which means, if their role within the organization is changed during that time, the SPA may present Calls-to-Action that are no longer relevant for the user. We attempt to overcome this using WebSockets and server-sent events; but, it definitely causes issues.
A user may have the SPA open for days at a stretch. Which means, if the client-side logic changes, it may be days before a given user refreshes their browser and can see those new changes. If those changes are are permissions-related, they may start making API calls against a back-end that no longer recognizes the user's authority.
Security considerations have to be duplicated on both the client and the server. We all know that we can't trust the client-side code to enforce anything. As such, even if we have security checks on the client, we have to make those same exact security checks on the server. So, in a way, by calculating any permissions on the client, we're violating the DRY (Don't Repeat Yourself) principle since we are duplication knowledge as to how Roles map onto CTAs.
The whole point of a View Partial is to provide the View with all relevant data that it needs to render. Historically, I've only thought about this in terms of the data itself, not in the user's ability to act on that data. As I said above, I've been making those security decisions in the client-side code. But, the more I think about it, the more I've come to realize that the view security considerations are just as much a part of the "view requirements" as the data is. And so, I've been starting to return various permissions
properties in the View Partial payloads.
To illustrate this, I've created a pseudo-code ColdFusion component that gathers the data for a "members listing page" within the Angular SPA. This ColdFusion component aggregates the active members, then pending invitations, and both top-level and row-level permissions
objects that dictate how the user can operate on this returned data.
In the following code, the "Membership Context" is the relationship between the currently authenticated user and the contextual organization (including the user's role within that organization). If there is no membership context, it means that the authenticated user is not an active member of the organization and therefore should be denied access.
component
accessors = true
output = false
hint = "I provide service methods for the members-list partial."
{
// ---
// PUBLIC METHODS.
// ---
/**
* I get main partial payload for the members-list page.
*/
public struct function getPartial(
required numeric authenticatedUserID,
required numeric companyID
) {
// SECURITY: Getting the membership context asserts that the given user is an
// active member of the given company. We need to make sure that the authenticated
// user has permission to even view this page. The resultant context information
// can then be used to authorize the subsequent data-access.
var membershipContext = companyService.getMembershipContext( companyID, authenticatedUserID );
var membership = membershipContext.membership;
var permissions = membershipContext.permissions;
if ( ! permissions.canViewMembers ) {
throwForbiddenError( arguments );
}
var members = getMembers( membershipContext );
var invitations = getInvitations( membershipContext );
return({
members: members,
invitations: invitations,
// The "partial" payload is ALWAYS A STRUCT - never an array. This way, we can
// always add new properties to it as the needs of the View layer evolve over
// time. The top-level "permissions" object here helps the View layer
// understand which CTAs (Calls to Action) can be rendered for this user.
permissions: {
canCreateInvitations: permissions.canCreateInvitations,
canDeleteInvitations: permissions.canDeleteInvitations,
canDeleteInvitationsByRoleID: permissions.canDeleteInvitationsByRoleID,
canChangeMemberRoles: permissions.canChangeMemberRoles,
canChangeMemberRolesByRoleID: permissions.canChangeMemberRolesByRoleID,
canRemoveMembers: permissions.canRemoveMembers,
canRemoveMembersByRoleID: permissions.canRemoveMembersByRoleID
}
});
}
// ---
// PRIVATE METHODS.
// ---
/**
* I return the members for this context that the authenticated user has permissions to
* view.
*/
private array function getMembers( required struct membershipContext ) {
var permissions = membershipContext.permissions;
// From both a security and a consumer standpoint, the raw memberships have more
// data than we want to return to the client. As such, we have to map the raw data
// onto a View-Model structure that the client can more easily use.
var rawMembers = dataAccess.getMembers( membershipContext );
var members = [];
for ( var member in rawMembers ) {
var avatarUrl = ( member.avatarID.len() )
? avatarService.getPublicUrl( member.avatarID )
: avatarService.getGenericAvatarUrl()
;
members.append({
id: member.id,
name: member.name,
email: member.email,
role: {
id: member.roleID,
name: memberRoles.getRoleName( member.roleID )
},
createdAt: member.createdAt.getTime(),
avatarUrl: avatarUrl,
// In addition to the top-level permissions structure, we're going to
// return a row-level permissions structure because the authenticated user
// cannot act uniformly on all of the data that we return. For example, a
// MANAGER might be able to remove another MANAGER from the organization;
// but, that same MANAGER cannot remove an ADMIN (since they are outranked
// by the member's role).
permissions: {
canRemove: permissions.canRemoveMembersByRoleID[ member.roleID ],
canChangeRole: permissions.canChangeMemberRolesByRoleID[ member.roleID ]
}
});
}
return( members );
}
// ... Incomplete on purpose (this is just pseudo-code) ... //
}
As you can see, I have two sets of permissions
being returned in this View Partial payload: one at the high-level which helps the View figure out which buttons and data to render; and, one at the row-level which specifically indicates how this user can act on this record. This should provide the Angular SPA with everything in needs to render the members list; but, without having to perform any security-related calculations of its own.
Now, when the AngularJS application goes to render the page, all it has to do is check the permissions
object to see what the user can do. In the following pseudo-code, notice how the ng-if
directives determine which DOM (Document Object Model) nodes can be rendered:
<button
ng-if="permissions.canCreateInvitations"
ng-click="inviteNewMembers()">
Invite new members
</button>
<table width="100%">
<thead>
<tr>
<td>
Member
</td>
<td>
Role
</td>
<td>
Actions
</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="member in members track by member.id">
<td>
{{ member.name }}
</td>
<td>
{{ member.role.name }}
</td>
<td>
<button
ng-if="member.permissions.canRemove"
ng-click="removeUser( member )">
Remove user
</button>
<button
ng-if="member.permissions.canChangeRole"
ng-click="changeRole( member )">
Change role
</button>
</td>
</tr>
</tbody>
</table>
No more messing around with the current user's role and comparing that role to the roles of members within the API data - I just look at the permissions outlined in the API and render accordingly. And, if the permissions are different in the next API response, no problem, the Angular app implicitly changes the View rendering during its View-model reconciliation digest phase.
This approach is still a work-in-progress (WIP) for me. But, I've been trying to use these permissions
properties on all the new View Partial API end-points that I've created in the last few months. And, it's definitely making my life much easier; and, reducing the amount of client-side that I have to write.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →