I would like to present the following approach:
In ReactApp, we can see that every time the root React-app is mounted, a context, ForemanContext is given along.
This context is therefore available to every child component.
The context contains, among others, information about the UI, the organization and pagination settings.
My proposal would be to pass the current user’s permissions to that context.
Since the permissions are unique, it makes sense to implement this using a data-structure with a fast access time. In this case, I chose Set, as, ideally, the .has method has a time complexity of O(1).
(ES6 mandates that this operation must complete in sublinear time, but any JS-engine worth it’s salt will do it in O(1))
I implemented a rough proof-of-concept and used it in the rewritten-using-React Ansible roles page I implemented recently:
Proof of concept implementation in Foreman:
Along with the contextified permissions, I would like to propose three ways of consuming them in frontend code:
- the
usePermissionhook
This custom hook takes a permission name and returns whether the current user has the given permission - the
Permittedcomponent
This component aims to extract the pattern of “render if permission”. It wraps the component that is supposed to be conditionally rendered and a permission’s name. Only if the user has the given permission, the component will be rendered. - using the
useForemanPermissionsanduseForemanContexthooks.
Intended for advanced use cases, theuseForemanPermissionshook gives access to the permissions set.
Reactified Ansible roles page
This is the original implementation of the React roles page. As with the old one, the button to import roles from a Smart Proxy is hidden if a user does not have the import_ansible_roles permission.
This is done by wrapping the button component, making an API request to query the user’s permission and rendering the button if the permission is granted.
Reactified Ansible roles page using the proof-of-concept
Here, I replaced the aforementioned API-based permission checking with the context-based solution.
As a result of this a lot of API-handling boilerplate can be omitted, as there is no more API-request. Furthermore, the time complexity of the permission-check was reduced from linear to constant time (in V8 at least).
On a side note: I think it would make sense in general to provide the permissions as constants as this makes refactoring a lot easier should it ever become necessary.
A few words about performance:
Since this approach uses a hash based data-structure instead of an array, the time-complexity of a single permission check has been reduced from O(n) to O(1). Consequentially, the time-complexity for multiple permission checks has been reduced from O(n²) to O(n) with n being the number of permission checks. Depending on how loaded a Foreman server is with plugins, it can make a difference. Furthermore, this approximation still leaves out the API-request.
The old, API based approach completes the request to fetch the current user’s permissions in around ~200ms (on a decently fast system).
I would try to profile the custom hook, but profiling React is just a pain. I am confident however that the hook completes much faster.
The loading time of the whole page is reduced by 100ms-150ms on average.
The mounting time of the root app is increased as is the metadata payload, but not by much as the front-end only requires the name of the permissions and is not concerned about id or ressource_type.
The data to fill the context is queried by the root-app every time it is mounted, which still happens quite often at the moment, but this will obviously decrease once more and more pages use client-side routing.
I can deep-dive into the performance implications if desired, but I think this already paints a pretty clear picture.
One downside:
At the moment, I can only think of one downside to this approach and that is stale context.
As mentioned above, the context is refreshed every time the React app is mounted. Usually, this happens when the page reloads.
In a full-fledged SPA scenario, it would be possible for a user to acquire a permission, which would not be acknowledged by the UI until it is reloaded and therefore, the context is refreshed.
Though, this will be a problem regardless of permissions and by the time we get there, the context would have to be refreshed or the page reloaded explicitly. I can’t think of an impromptu example, but I don’t think big-corp SPAs like MS Teams are above this…
As of right now though, this can not happen through the UI, since every management UI that can grant permissions to users is completed by pressing “Submit”, which reloads the page, as it is rendered server-side.
Yeah, a bit of a long one, so thanks a lot for reading and I’m very much looking forward to your opinions on this!