Base solution for your next web application
Open Closed

Inconsistent Role Switching Behavior in Production App on Azure #11789


User avatar
0
sms.environmental created

Hi,

We are encountering an unusual issue with our production application hosted on Azure App Service. Our application includes a functionality where administrative users can toggle between 'Admin' and 'Client' modes. This feature is designed to switch user roles: 'Admin', with full permissions, and 'Client', with restricted permissions. The process involves temporarily replacing the 'Admin' role with the 'Client' role for the user, and vice versa. To facilitate this, we maintain a snapshot of the user's original roles, ensuring they can revert back seamlessly.

However, in the production environment, we've observed inconsistent behavior when switching from 'Client' back to 'Admin' mode. Specifically, the full set of 'Admin' permissions and menu options do not always appear immediately. Even after refreshing the browser, the application sometimes continues to display only 'Client' menu options. After several refresh attempts, the correct 'Admin' options appear, but the issue can recur unpredictably. Additionally, we've encountered scenarios where, upon switching to 'Admin' mode, navigating to an admin-only page results in an error message about insufficient permissions, despite the page loading in the background.

This erratic behavior seems to suggest a potential caching issue, but it's perplexing as this problem does not occur in our local environment. We've tried executing _cacheManager.GetUserPermissionCache().Clear(); when we switch between the modes but this didn't help. We also tried _signInManager.SignInAsync(user, false); to force the user information cookie to be re-issued to the browser but that did not change anything either.

We don't use Redis.

Do you have any suggestions on what could be causing this?


7 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @sms.environmental

    As you stated, this sounds like a cache issue. Is it possible to share your code which implements this Role Switching feture ?

  • User Avatar
    0
    sms.environmental created

    Hi ismcagdas,

    This is our the API method that our Role Switching feature calls from Client Mode and Exit Client Mode buttons in the front-end (it is also called where necessary during login to automatically exit Client Mode for users when they have had to re-enter login credentials):

            public async Task<long> SwitchUserRole(bool isClientRole, long userId)
            {
                User user = UserManager.Users.Include(x => x.Roles).Where(x => x.Id == userId).FirstOrDefault();
                if (isClientRole)
                {
                    // save current roles and switch to Client role only (direclty-assigned user permissions are not added/removed)
                    foreach (var item in user.Roles)
                    {
                        var userRoleHistory = new UserRoleSwitchHistory();
                        userRoleHistory.UserId = AbpSession.UserId;
                        userRoleHistory.PreviousRoleId = item.RoleId;
                        await _userRoleSwitchHistory.InsertAndGetIdAsync(userRoleHistory);
                    }
                    string[] roles = { "Client" };
                    await UserManager.SetRolesAsync(user, roles);
                    user.IsSwitchedToClientRoleForViewing = true;
    
                    CheckErrors(await UserManager.UpdateAsync(user));
    
                    await LogClientAdminModePermissions(true, false, "SwitchUserRole - 1 - AFTER setting client user role");
    
                    // NB removed for moment as may not be needed
                    //// NOD-2141 NB this should cause the user info cookie to be re-ussed to the browser updating the permissions information
                    //await _signInManager.SignOutAsync();
                    //await _signInManager.SignInAsync(user, false);
    
                    //// NOD-2141 Added to clear cached user permissions
                    await ForceRefresh();
    
                    await LogClientAdminModePermissions(true, false, "SwitchUserRole - 2 - AFTER ForceRefresh() call");
    
                    // ensure some cached user information affecting SessionAppService.IsClient/IsContract etc. is correctly updated for the revised role(s) representing Client mode i.e. ensures IsClient returns true
                    // NB we're not doing anything with the results in output - just calling the function rebuilds the cached user information
                    var output = await _sessionAppService.GetCurrentLoginInformations();
    
                    return user.Id;
                }
                else
                {
                    // remove current role(s) (probably Client one) and restore previous stored roles (direclty-assigned user permissions are not added/removed)
                    var usersRole = _userRoleSwitchHistory.GetAll().Where(x=>x.UserId==user.Id).ToList();
                    var roles=  _roleManager.Roles.Where(x => usersRole.Select(xx => xx.PreviousRoleId).Contains(x.Id)).Select(x=>x.Name);
                    await UserManager.SetRolesAsync(user, roles.ToArray());
                    await _userRoleSwitchHistory.HardDeleteAsync(x => x.UserId == user.Id);
    
                    // ensure some cached user information affecting SessionAppService.IsClient/IsContract is correctly updated for the restored previous role(s)
                    // NB we're not doing anything with the results in output - just calling the function rebuilds the cached user information
                    var output = await _sessionAppService.GetCurrentLoginInformations();
                    user.IsSwitchedToClientRoleForViewing = false;
    
                    CheckErrors(await UserManager.UpdateAsync(user));
    
                    await LogClientAdminModePermissions(false, true, "SwitchUserRole - 1 - AFTER restoring previous user roles");
    
                    // NB removed for moment as may not be needed
                    //// NOD-2141 NB this should cause the user info cookie to be re-ussed to the browser updating the permissions information
                    //await _signInManager.SignOutAsync();
                    //await _signInManager.SignInAsync(user, false);
    
                    //// NOD-2141 Added to clear cached user permissions
                    await ForceRefresh();
    
                    await LogClientAdminModePermissions(false, true, "SwitchUserRole - 2 - AFTER ForceRefresh() call");
    
                    return user.Id;
                }
            }
    

    And this is the ForceRefresh() method we tried to clear the relevant parts of the cache in an attempt to try and get the cached permissions in the front-end to refresh:

    // NOD 2141 - Added based on https://stackoverflow.com/questions/49769936/refresh-permissions-from-in-memory-cache-with-asp-net-boilerplate
            //          - Intended for use in SwitchUserRole() above to force a refresh of cached used permissions in the back-end
            //            and hopefully in the front-end too
            private async Task <bool> ForceRefresh()
            {
                // Clear the user permission cache to force a refresh of it's contents when next used
                await _cacheManager.GetUserPermissionCache().ClearAsync();
    
                // lets try this too
                await _cacheManager.GetRolePermissionCache().ClearAsync();
    
                return true;
            }
    

    And this is just some just some custom logging to Logs.txt that we added to try and understand what is happening in the back-end role switching on Azure:

            // NOD 2141 - Added for logging permission checks so we can confirm that they are as expected in the back-end
            //            in dev environment or on Azure irrespective of what is happenning in the front-end
            private async Task <bool> LogClientAdminModePermissions(bool clientMode, bool adminMode, string title)
            {
                if (clientMode)
                {
                    Logger.Info(String.Empty);
                    Logger.Info($"CLIENT MODE - {title}");
    
                    bool b1 = await PermissionChecker.IsGrantedAsync(AppPermissions.Data_AlwaysRestrictContractsAndBuildings);
                    bool b2 = await PermissionChecker.IsGrantedAsync(AppPermissions.Pages_Administration);
                    bool b3 = await PermissionChecker.IsGrantedAsync(AppPermissions.Pages_Settings_General);
                    bool b4 = await PermissionChecker.IsGrantedAsync(AppPermissions.Pages_SalesInvoicing);
    
                    Logger.Info($"PermissionChecker.IsGrantedAsync(AppPermissions.Data_AlwaysRestrictContractsAndBuildings) = {b1}  (TRUE expected)");
                    Logger.Info($"PermissionChecker.IsGrantedAsync(AppPermissions.Pages_Administration) = {b2}  (FALSE expected)");
                    Logger.Info($"PermissionChecker.IsGrantedAsync(AppPermissions.Pages_Settings_General) = {b3}  (FALSE expected)");
                    Logger.Info($"PermissionChecker.IsGrantedAsync(AppPermissions.Pages_SalesInvoicing) = {b4}  (FALSE expected)");
    
                    Logger.Info(String.Empty);
                }
    
                if (adminMode)
                {
                    Logger.Info(String.Empty);
                    Logger.Info($"ADMIN MODE - {title}");
    
                    bool b1 = await PermissionChecker.IsGrantedAsync(AppPermissions.Data_AlwaysRestrictContractsAndBuildings);
                    bool b2 = await PermissionChecker.IsGrantedAsync(AppPermissions.Pages_Administration);
                    bool b3 = await PermissionChecker.IsGrantedAsync(AppPermissions.Pages_Settings_General);
                    bool b4 = await PermissionChecker.IsGrantedAsync(AppPermissions.Pages_SalesInvoicing);
    
                    Logger.Info($"PermissionChecker.IsGrantedAsync(AppPermissions.Data_AlwaysRestrictContractsAndBuildings) = {b1}  (FALSE expected)");
                    Logger.Info($"PermissionChecker.IsGrantedAsync(AppPermissions.Pages_Administration) = {b2}  (TRUE expected)");
                    Logger.Info($"PermissionChecker.IsGrantedAsync(AppPermissions.Pages_Settings_General) = {b3}  (TRUE expected)");
                    Logger.Info($"PermissionChecker.IsGrantedAsync(AppPermissions.Pages_SalesInvoicing) = {b4}  (TRUE expected)");
    
                    Logger.Info(String.Empty);
                }
    
                return true;
            }
    

    So above we are just logging some checks on a few permissions (that we expect to change with role changes) a couple of times for each role switch/toggle - once after swapping the roles and again after forcing cache refreshes.

    Please let us know if you need any more information - we are still experiencing the same behaviour where the permission changes are picked up instantly by the browser when running in the development-environment but when running from Azure the permission changes seem to be cached in the browser and don't get picked until after a few menu clicks and/or page reloads. NB the role switch buttons in the front-end do navigate to the home/dashboard page in he. application after a role switch

  • User Avatar
    0
    ismcagdas created
    Support Team
  • User Avatar
    0
    sms.environmental created

    Hi,

    Thanks for your your response. During the hours we experiencing these issues the app is scaled out to 4 different instances. Could this be related?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Thanks. If that's the case, you must use a distributed cache, see https://docs.aspnetzero.com/en/aspnet-core-mvc/latest/Clustered-Environment#switching-to-a-distributed-cache

  • User Avatar
    0
    sms.environmental created

    Hi,

    Thank you for confirming that and providing a link - we figured out that must be the problem as we realised we only experienced the issue we described while the app scaled up to 4 instances and not at all while running 1 instance.

    I guess we'll find out more from that link but is everything we need to implement and use a Distributed Cache built-into the framework and/or Azure hosting?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Yes, some of them are implemented directly in ASP.NET Zero and scaling SignalR can be achieved by using ASP.NET Core's SignalR scaling.