Dynamic Role Considerations
- Jacques Marais
Overview
Helium and the Helium DSL provides built-in role based authorisation as well as mechanisms for managing app roles that are assigned to users. In some cases, however, a more dynamic system of managing roles is preferable. In such cases an app might use various internal roles of which multiple can be assigned to a user for each "Helium provided" app role.
Since these dynamic roles are not managed by Helium itself but rather by app logic some risks and mitigation strategies need to be considered to ensure users are not allowed to access functionality and data that they are not authorised for.
App Example
The rest of this document will refer to a sample app that demonstrates a minimal example of dynamic, application managed (as apposed to platform managed) roles.
Consider the following code snippets representing an appropriate model for managing of dynamic user roles:
@Role("Web User") persistent object WebUser { string name; string mobile; @ManyToMany WebUserRole roles via users; }
persistent object WebUserRole { string name; }
Taking the above into consideration consider the following data representing 4 roles and a single user with three of the four roles assigned to them. Note the data here is generated as part of the automatic / built-in app invitation. This is for demonstration purposes only and should not be considered best practice for general bulk data generation or inserts.
@InviteUser WebUser getWebUser() { // Create the default user WebUser webUser = WebUser:new(); webUser.name = "Test User"; webUser.mobile = "27761112222"; webUser.save(); // Create and assign roles to the user createRoles(webUser); return webUser; } void createRoles(WebUser webUser) { // Create roles 1 - 3 and assign to the user WebUserRole role1 = WebUserRole:new(); role1.name = "role1"; role1.users.append(webUser); role1.save(); WebUserRole role2 = WebUserRole:new(); role2.name = "role2"; role2.users.append(webUser); role2.save(); WebUserRole role3 = WebUserRole:new(); role3.name = "role3"; role3.users.append(webUser); role3.save(); // Create role 4 but do not assign it the the user WebUserRole role4 = WebUserRole:new(); role4.name = "role4"; role4.save(); }
helium-app-1=# select * from webuser; -[ RECORD 1 ]-+------------------------------------- _id_ | 12e5351f-ff6b-4ac7-9816-bff118a72e84 _tstamp_ | 2021-07-07 16:51:07.824271 name | Test User mobile | 27761112222 _firstnames | Test _nickname | _surname | User _locale | pt _timezone | Africa/Johannesburg _tx_id_ | 44267184 _change_type_ | update _change_seq_ | 15
helium-app-1=# select * from webuserrole ; _id_ | _tstamp_ | name | _tx_id_ | _change_type_ | _change_seq_ --------------------------------------+----------------------------+-------+----------+---------------+-------------- 2c4840f5-3181-433e-a731-f4545237f3f9 | 2021-07-07 16:51:07.249882 | role1 | 44267173 | create | 4 f3a7b0af-d2b5-411a-a1a9-94a8c6b48b2a | 2021-07-07 16:51:07.249882 | role2 | 44267173 | create | 7 98c6bd22-756c-421f-be73-c175b17f376a | 2021-07-07 16:51:07.249882 | role3 | 44267173 | create | 10 fca8fe35-90d9-4076-96b3-a79af35af53b | 2021-07-07 16:51:07.249882 | role4 | 44267173 | create | 13
helium-app-1=# select * from webuser_roles ; webuser_fk | webuserrole_fk | _tstamp_ | _tx_id_ | _change_seq_ --------------------------------------+--------------------------------------+----------------------------+----------+-------------- 12e5351f-ff6b-4ac7-9816-bff118a72e84 | 2c4840f5-3181-433e-a731-f4545237f3f9 | 2021-07-07 16:51:07.249882 | 44267173 | 5 12e5351f-ff6b-4ac7-9816-bff118a72e84 | f3a7b0af-d2b5-411a-a1a9-94a8c6b48b2a | 2021-07-07 16:51:07.249882 | 44267173 | 8 12e5351f-ff6b-4ac7-9816-bff118a72e84 | 98c6bd22-756c-421f-be73-c175b17f376a | 2021-07-07 16:51:07.249882 | 44267173 | 11
With data model and test data consider the following view and unit:
<?xml version="1.0" encoding="UTF-8"?> <ui xmlns="http://uiprogram.mezzanine.com/View" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://uiprogram.mezzanine.com/View"> <view label="view_heading.mainmenu" unit="MainMenuUnit" init="init"> <menuitem label="menu_item.mainmenu"> <userRole>Web User</userRole> </menuitem> <select label="select.available_roles"> <binding variable="selectedRole"/> <collectionSource function="getAvailableRoles"> <displayAttribute name="name"/> </collectionSource> </select> <submit label="button.update_role" action="updateRoleSelection"/> <!-- This widget is a placeholder and might represent a menu or other app functionality in practice --> <info label="info.current_role_label" value="info.current_role_content"/> </view> </ui>
unit MainMenuUnit; WebUserRole selectedRole; string currentRoleName; void init() { setViewState(); } // Get the roles that have been assigned to this user WebUserRole[] getAvailableRoles() { if(Mez:userRole() == "Web User") { WebUser currentUser = WebUser:user(); return currentUser.roles; } } // Set the views state based on the current user and selected roles void setViewState() { WebUserRole[] availableRoles = getAvailableRoles(); // If roles are available but not selected default to the first value if(availableRoles.length() > 0 && selectedRole == null) { selectedRole = availableRoles.get(0); } if(selectedRole != null) { currentRoleName = selectedRole.name; return null; } currentRoleName = "You either have no roles assigned to you"; return null; } // Submit user role selection void updateRoleSelection() { setViewState(); return null; }
The app described above contains a single view with a role dropdown that lists all the roles (as defined in the app logic) that has been assigned to the user. In also contains a submit button to submit the role selection and an info widget for which the state (value being displayed) is determined by the selected role.
Authorisation Bypass
Although a detailed example will not be provided here the concept is as follows: If the uuid for a role is known, a user, that has access to the application but does not have access to that role, can generate calls to Helium that updates the role selection to one that is not available in his role dropdown. If the view is then reloaded for that menu item the user might have access to functionality for a role that they have not been assigned.
Using this concept and applying it to our example app, it is possible that the user that does not have role4 assigned to them, can access the app functionality for role4 given that the id for the object instance (WebUserRole)
that represents role4 is known:
Mitigation
Fundamentally the authorisation bypass risk highlighted here can be mitigated by using additional validation that can be used to determine what view or application state should be presented to the user. We will describe three mechanism in the Helium DSL that can be used to achieve this.
Dynamically determine the state of the widgets being displayed
In this case we will include additional validation that needs to pass before the view state is determined. The state of the widgets that are displayed to the user can then be set in such a way that the user does not have access to app functionality that they are not authorised for.
In the case where a widget represents a menu that uses a data table the contents of the table can simply be omitted resulting in a scenario where no further functionality is available to the "unauthorised" user.
In our case we can set the state of the info widget to demonstrate the concept.
First, consider the additional validation function added to the MainMenuUnit
unit as well as its usage in the setViewState
method:
// Validate that the current user has the selected roles assigned to them bool validateRole() { WebUserRole[] availableRoles = getAvailableRoles(); if(availableRoles.length() == 0) { return false; } foreach(WebUserRole availableRole: availableRoles) { if(availableRole.name == selectedRole.name) { return true; } } return false; }
// Set the views state based on the current user and selected roles void setViewState() { WebUserRole[] availableRoles = getAvailableRoles(); // If roles are available but not selected default to the first value if(availableRoles.length() > 0 && selectedRole == null) { selectedRole = availableRoles.get(0); } if(selectedRole != null && validateRole() == true) { currentRoleName = selectedRole.name; return null; } currentRoleName = "You either have no roles assigned to you or you are trying to access functionality for which you are not authorised"; return null; }
Attempting to access the functionality using a role for which the user is not authorised will now result in the following in our example app:
Dynamically hide widgets
Similarly to the previous example, the same validation can be used to determine whether view widgets should be displayed at all. In the example app this is achieved by modifying our setViewState
method to set a visibility flag for all widgets on the view and then use the visibility bindings on the view itself:
bool displayWidgets; . . . // Set the views state based on the current user and selected roles void setViewState() { WebUserRole[] availableRoles = getAvailableRoles(); // If roles are available but not selected default to the first value if(availableRoles.length() > 0 && selectedRole == null) { selectedRole = availableRoles.get(0); } if(selectedRole != null && validateRole() == true) { currentRoleName = selectedRole.name; displayWidgets = true; return null; } displayWidgets = false; return null; } . . .
<?xml version="1.0" encoding="UTF-8"?> <ui xmlns="http://uiprogram.mezzanine.com/View" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://uiprogram.mezzanine.com/View"> <view label="view_heading.mainmenu" unit="MainMenuUnit" init="init"> <menuitem label="menu_item.mainmenu"> <userRole>Web User</userRole> </menuitem> <select label="select.available_roles"> <visible variable="displayWidgets"/> <binding variable="selectedRole"/> <collectionSource function="getAvailableRoles"> <displayAttribute name="name"/> </collectionSource> </select> <submit label="button.update_role" action="updateRoleSelection"> <visible variable="displayWidgets"/> </submit> <info label="info.current_role_label" value="info.current_role_content"> <visible variable="displayWidgets"/> </info> </view> </ui>
With this in place in our example app attempting unauthorised access to the view will result in the following:
In this case Helium attempts to load the view but fails due to the fact that no widgets are visible on the view. If the visibility binding was not applied to all widgets on the view, the view would load while hiding those widgets to which the invisibility bindings were applied.
More information on visibility binding can be found here.
Using built-in dynamic menu items
The final method that will be discussed here is to use the same validation as before and then use Helium's built-in dynamic menu item functionality to prevent unauthorised users from accessing the built-in app menu items entirely.
To implement this in our example app we modify the menu item on the view as follows:
<menuitem> <dynamicUserRoles function="getRolesForMainMenu"/> <dynamicLabel function="getLabel"/> </menuitem>
We also add the accompanying methods to the MainMenuUnit
unit:
// Authorise dynamic rendering of menu items DSL_ROLES[] getRolesForMainMenu() { setViewState(); DSL_ROLES[] roles; if(validateRole() == true) { roles.append(DSL_ROLES.Web_User); } return roles; } string getLabel() { return String:translate("menu_item.mainmenu"); }
Attempting to access the functionality using a role for which the user is not authorised will now result in the following in our example app:
In this case Helium attempts to find a menu item that is applicable to the user in order to load the associated view. Due to the validation, however, getRolesForMainMenu
returns an empty list and thus no menu item is available to the unauthorised user.
This will be accompanied by the following message in the logs:
Caused by: com.mezzanine.program.web.exception.SolutionException: There are no menu items for the current role, Web User. At least one view with a menu item is required.
More information on dynamic menu items can be found here.
Conclusion & Resources
In conclusion this document described how app managed dynamic roles can expose a security vulnerability by means of authorisation bypass.
We also demonstrated how additional validation logic could be used in conjunction with general widget state, widget visibility bindings and dynamic menu items to deny access to app functionality for unauthorised users.
Also see the following resources:
- Dynamic menu items in the Helium DSL
- Visibility bindings for widgets in the Helium DSL
- Jira ticket for reference: HE-8823
- Sample app source code: dyn-roles.zip