RequiredPermission overwrites Session.Permissions

Hello Servicestack,
I’m experiencing a strange behavior when I decorate a Request DTO with RequiredPermission.
Here’s the scenario :

We’ve overwritten the AuthUserSessions with the following code:

    public class IFMSAuthEvents : IAuthEvents
    {
        public void OnAuthenticated(IRequest httpReq, IAuthSession session, IServiceBase authService, IAuthTokens tokens, Dictionary<string, string> authInfo)
        {
            
                var container = ServiceStackHost.Instance.Container;
                IEnumerable<UserPermissions> permissions;
                IEnumerable<UserRoles> roles;
                
                using (var db = container.ResolveNamed<IDbConnectionFactory>(Core.Resources.IFMSDbFactory).Open())
                {
                    permissions = db.Select<UserPermissions>(x => x.UserName == session.UserName || x.UserName == session.UserAuthName);
                    roles = db.Select<UserRoles>(x => x.UserName == session.UserName || x.UserName == session.UserAuthName);
                }
    
                if (session.Permissions == null)
                    session.Permissions = new List<string>();
    
                foreach (var p in permissions)
                {
                    if (!session.Permissions.ContainsIgnoreCase(p.PermissionName))
                        session.Permissions.Add(p.PermissionName);
                }
    
                if (session.Roles == null)
                    session.Roles = new List<string>();
    
                foreach (var r in roles)
                {
                    if (!session.Roles.ContainsIgnoreCase(r.RoleName))
                        session.Roles.Add(r.RoleName);
                }
                httpReq.SaveSession(session);
    
                var userSession = session as IFMSAuthUserSession;
                if (userSession != null)
                {
                    var modules = userSession.GetAuthorizedModules();
                    modules.ForEach(m => Log.Information("Found module {Name}", m.Name));
    
                    var menu = userSession?.Menu;
                    if (menu == null)
                    {
                        if (AppHost.AppSettings.Get(Core.Resources.PersistSessionKey, true))
                        {
                            httpReq.SaveSession(session);
                        }
    
                        menu = session.BuildMenu(modules);
    
                        if (userSession != null)
                        {
                            userSession.Menu = menu;
                        }
                    }
    
                }
        }
    
        public void OnCreated(IRequest httpReq, IAuthSession session)
        {
            // Method intentionally left empty.
        }
    
        public void OnLogout(IRequest httpReq, IAuthSession session, IServiceBase authService)
        {
            Log.Information("User {UserAuthName} logout", session.UserAuthName);
            ServiceStackHost.Instance.Container.Resolve<ICacheClient>().Remove(SessionFeature.GetSessionKey(httpReq.GetSessionId()));
        }
    
        public void OnRegistered(IRequest httpReq, IAuthSession session, IServiceBase registrationService)
        {
            // Method intentionally left empty.
        }
    }

The DTO that’s facing problem is the following

    [Route(Route, Verbs = "POST")]
    [Authenticate]
    [RequiredPermission(Route)]
    public class FileDownload : INullableFileType, ITenant<int?>, IReturn<FileStreamResult>
    {
        const string Route = "/file/download";

        public int? FileId { get; set; }

        public int? FileTypeId { get; set; }

        public int? TenantId { get; set; }
    }

I’m using ServiceStack’s API Key as AuthenticationProvider

After authentication , in the OnAuthenticatedMethod I’m filling property Permissions with values fetched from DB.

My implementation of AuthUserSession is

  public class IFMSAuthUserSession : AuthUserSession
#pragma warning restore S101 // Types should be named in camel case
    {
        public IFMSAuthUserSession()
        {
            CustomInfo = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
        }

        IList<IModule> AuthorizedModules { get; set; }

        IModule CurrentModule => HostContext.Resolve<IModuleResolver>().CurrentModule;

        public IMenu Menu { get; set; }

        public IList<IModule> GetAuthorizedModules() =>
            AuthorizedModules
            ?? (AuthorizedModules = Core.Resources.ModuleResolver.GetAuthorizedModules(this));

        public string UserImpersonated { get; set; }

        //Modificato IDictionary in Dictionary perché servicestack prevede degli extension method solo per Dictionary
        public Dictionary<string, object> CustomInfo { get; set; }

        /// <summary>
        /// Restituisce se l'utente ha tutti i ruoli specificati.
        /// </summary>
        public bool HasAllRoles(IEnumerable<string> roles, IAuthRepository authRepo)
        {
            
            return roles.All(y => HasRole(y, authRepo));
        }

        /// <summary>
        /// Restituisce se l'utente ha almeno uno dei ruoli specificati.
        /// </summary>
        public bool HasAnyRoles(IEnumerable<string> roles, IAuthRepository authRepo)
        {
            return roles.Any(y => HasRole(y, authRepo));
        }

        /// <summary>
        /// Restituisce se l'utente ha tutti i permessi specificati.
        /// </summary>
        public bool HasAllPermission(IEnumerable<string> permissions, IAuthRepository authRepo)
        {
            return permissions.All(y => HasPermission(y, authRepo));
        }


        /// <summary>
        /// Restituisce se l'utente ha almeno uno dei permessi specificati.
        /// </summary>
        public bool HasAnyPermission(IEnumerable<string> permissions, IAuthRepository authRepo)
        {
            return permissions.Any(y => HasPermission(y, authRepo));
        }

        /// <summary>
        /// Se il module implementa l'interfaccia <see cref="IHasPermission"/> allora si utilizza la logica implementata, altrimenti si utilizza la logica di default.
        /// </summary>
        /// <param name="permission"></param>
        /// <returns></returns>

        public override bool HasPermission(string permission, IAuthRepository authRepo)
        {
            var module = CurrentModule as IHasPermission;

            return module?.HasPermission(permission) ?? Permissions != null && Permissions.Contains(permission);
        }

        /// <summary>
        /// Se il module implementa l'interfaccia <see cref="IHasRole"/> allora si utilizza la logica implementata, altrimenti si utilizza la logica di default.
        /// </summary>
        /// <param name="role"></param>
        /// <returns></returns>
        public override bool HasRole(string role, IAuthRepository authRepo)
        {
            var module = CurrentModule as IHasRole;

            return module?.HasRole(role) ?? Roles != null && Roles.Contains(role);
        }
    }

In the HasPermission method sometimes I’ve got property Permissions correctly filled, in the same session Permissions become empty. Doing some debugging I’ve seen that the CallStack is the following

It seems that the method UpdateFromUserAuthRepo overwrites the property Permissions

Any workaround?

Note your Filter Attributes like [Authenticate] and [RequiredPermission] should be on your Service Implementations so your Service Models don’t have any dependencies except for ServiceStack.Interfaces and can be reused.

You should also avoid using Interfaces in data models, i.e. use List<T> instead of IList<T> especially interfaces like IMenu, this also goes for serializing unknown object types in Dictionary<string, object> as well, you’ll have a lot less runtime issues by only serializing known types, e.g. Dictionary<string, string>. I’m surprised this UserSession persists without issue, if you’re using the default InMemory Cache then its not serializing and I suspect will fail when using a distributed caching provider where it would need to be serialized.

As for your issue, this is the implementation of UpdateFromUserAuthRepo:

public static void UpdateFromUserAuthRepo(this IAuthSession session, IRequest req, IAuthRepository userAuthRepo = null)
{
    if (session == null)
        return;

    if (userAuthRepo == null)
        userAuthRepo = HostContext.AppHost.GetAuthRepository(req);

    if (userAuthRepo == null)
        return;

    using (userAuthRepo as IDisposable)
    {
        var userAuth = userAuthRepo.GetUserAuth(session, null);
        session.UpdateSession(userAuth);
    }
}

public static void UpdateSession(this IAuthSession session, IUserAuth userAuth)
{
    if (userAuth == null || session == null) return;
    session.Roles = userAuth.Roles;
    session.Permissions = userAuth.Permissions;
}

Where it’s updating the User Session from the Users Roles/Permissions stored in the User Auth Repo which I’m assuming you’ve not kept updated with the Roles/Permissions on the Users Session. Anyway I’ve modified it to merge the Roles/Permissions instead of overwriting them in this commit. This change is available from v4.5.13 that’s now available on MyGet.

Thanks for your reply, When 4.5.13 will be avaiable? Since it’s a static method do you think If I use the same namespace and method declaration of UpdateSession to use it as hack till the 4.5.13 will be released?
Thanks

No extension methods cannot be overridden like that.

Excuse me ,
I’ve branched my solution to test the .13 … I’ve got some issues on ServiceStack.Interfaces I got this error

Could not install package ‘ServiceStack.Interfaces 4.5.6’. You are trying to install this package into a project that targets ‘Unsupported,Version=v0.0’, but the package does not contain any assembly references or content files that are compatible with that framework. For more information, contact the package author. 0

But when I update I got it from the 4.5.6 → 4.5.13

Any suggestion?

You’re trying to install v4.5.6 ServiceStack.Interfaces NuGet Package which does not exist on MyGet. The only version of MyGet is v4.5.13: https://www.myget.org/feed/servicestack/package/nuget/ServiceStack.Interfaces

This is likey a bad/partial upgrade, there are no v4.5.6 NuGet packages on MyGet and none of the NuGet packages on MyGet references v4.5.6. The only reference to v4.5.6 would be in your packages.config, you should upgrade all packages in your entire solution.

Been able to consolidate references, tried, everything works as expected now…wait for the 4.5.13 on nuget to upgrade the project on the trunk, Thanks!

1 Like

Odd numbers like v4.5.13 are pre-release packages are only on MyGet, the next version on NuGet will be v4.5.14 but since we just released that likely won’t be for another ~6 weeks.