0

I have a solution that is comprised of two projects, a Blazor server website and a Class Library. In the class library, I created a class to allow me to add custom attributes to functions in other classes in the library. This allows me to decorate a function with roles that can access the function. I then have a class that gets called to pull those tags and checks the database for consensus. This works fine, when the calling function is a regular function. But, if it is an async function, the stack frame I am looking at returns a value of "MoveNext" instead of the name of the method. Is there a way to get the method name of the calling method if that method is async? Here is my pseudo code:

Class to add custom attributes:

namespace DataAccessLibrary
{
    [AttributeUsage(AttributeTargets.Method |AttributeTargets.ReturnValue)]
    public class MELAdvanceRoles: Attribute
    {
        public string[] _Roles { get; set; }
        public MELAdvanceRoles(string[] roles)
        {
            _Roles = roles;           
        }
    }
}

Example function that uses custom attributes:

namespace DataAccessLibrary
{
    public class ProgramData: IProgramData
    {
        private readonly ISqlDataAccess _db;      
        private readonly IUserRights _userRights;

        public ProgramData(ISqlDataAccess db, IUserRights userRights)
        {
            _db = db;            
            _userRights = userRights;
        }   
        [MELAdvanceRoles(new string[] { "MANAGER", "EMPLOYEE", "READ_ONLY" })]
        public async Task<int> UpdateProgram(ProgramModel program)
        {   
            if (_userRights.CanUserEditProgram(program.id))
                //protected stuff
            }
            else
            {
                throw new Exception("403 Forbidden");
            }                
        }
}

CanUserEditProgram Function in _userRights

public bool CanUserEditProgram(int program_id)
{         
    //get roles in the custom attribute  
    string[] allowed_roles = GetMethodRoles();
    // use that list for database stuff
}

public string[] GetMethodRoles()
{
    string[] roles = { };
    StackTrace stackTrace = new StackTrace();
    //it seems that functions that are async are not on the second frame
    string methodName = stackTrace.GetFrame(2).GetMethod().Name;
    //mehtodname returns 'MoveNext' if the calling method is async
    MethodInfo mInfo = stackTrace.GetFrame(2).GetMethod().ReflectedType.GetMethod(methodName);
    //which means mInfo returns null
    bool isDef = Attribute.IsDefined(mInfo, typeof(MELAdvanceRoles));
    if (isDef)
    {
        MELAdvanceRoles obsAttr = (MELAdvanceRoles)Attribute.GetCustomAttribute(mInfo, typeof(MELAdvanceRoles));
        if (obsAttr != null)
        {
            roles = obsAttr._Roles;
        }
    }
    return roles;
}

Thanks

jason
  • 3,821
  • 10
  • 63
  • 120
  • 2
    While I applaud your effort to standardise your security like this, I think it's a pretty flaky solution, one reason you have already found. It's also very slow since getting stack traces is particularly inefficient. – DavidG Oct 12 '21 at 16:28
  • @DavidG yes, I am not happy with the stack option either. Is there a better way to handle this? Reflection or some other way to get the attributes? – jason Oct 12 '21 at 16:32
  • Would the parameter attribute [CallerMemberName] on a default parameter value from System.Runtime.CompilerServices.CallerMemberNameAttribute work in this case? -> public string[] GetMethodRoles( [CallerMemberName] methodName="") – Ross Bush Oct 12 '21 at 16:35
  • @RossBush yes, I considered passing the method name as well. But, it doesn't seem as clean a solution. The reason I added the custom attributes to the functions is to allow an easy way for future developers easily add/remove tags as needed and to abstract away some of the complexities. I would like to avoid requiring additional coupling if possible – jason Oct 12 '21 at 16:43
  • The parameter is optional and if not supplied it will default to the value of the attribute. You can still call the method like var x = GetMethodRoles(), however, inside the function the actual parameter value of methodName will equal the calling function, unless overriden. -> https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.callermembernameattribute?view=net-5.0 – Ross Bush Oct 12 '21 at 16:53
  • Any solution you come up with is going to be highly dependent on implementation details subject to change over time. You also have the same problem with iterator blocks, anonymous methods, etc. that all perform major refactors of the code in ways that won't necessarily stay the same over time. So not only do you need to figure out how to get the information you need for every refactor the compiler can do, you need to do it for every past, present, and future version of the compiler. You also don't necessarily know what compiler version the code you're running was compiled on. – Servy Oct 12 '21 at 16:53
  • @Servy so there is no way for me to have my GetMethodRoles() method reliably determine the custom attributes of calling method? What is the use case of custom attributes then? Maybe I am thinking about this incorrectly? – jason Oct 12 '21 at 16:58
  • @jason You can determine the calling method based on the methods the compiler actually ends up turning everything into. That may not be the calling method in the source code that generated it. Attributes are useful because you can reliably get information about any given method you have chosen, the problem in your case is you can't figure out which method you actually care about, not that you can't get the attributes from a method you've got a reference to. – Servy Oct 12 '21 at 17:01
  • @RossBush I took a look at what you suggested. Those properties return the method name but not the class calling the function. It does return the CS file, which I could maybe parse for the class name and cast accordingly. I could also add additional attributes for MethodName and ClassName. Both options seem like work arounds. I was hoping for something more elegant. But, it is an option so thanks! – jason Oct 12 '21 at 18:54
  • [Polite] It looks like a crazy idea to me, but as the saying goes - it's your funeral!. Why do you want to control access in a Library? It makes the library application specific, so why have a library in the first place. Surely you control access in the components that use the library. If you want checking on a method level in the component, you can write an `AppUser` service that interfaces with `User` in `AuthenticationState` to read out the roles and check them when you call a library method. Another crazy idea, why not pass say the `ClaimsPrincipal` to each method and check the roles? – MrC aka Shaun Curtis Oct 12 '21 at 19:31
  • @MrCakaShaunCurtis I appreciate the sentiment and would like more feedback. Yes, my Web application has built in authorization that works fine. I use the Custom Attributes on my Class Library less as security and more for data access. For Example, In my application I have a table of Programs and a table of ProgamManagers. ProgramManagers has an ID to the Program and a User as well as their role (Manager, Admin, Readonly for example.) In My class lib, I might have a function that returns all Programs where the user has Admin Role (which is a role listed in the custom Attribute). – jason Oct 12 '21 at 22:17
  • It addition, while my Web Application has role/claim based access to the pages, I want to make sure my Class Library is also protected – jason Oct 12 '21 at 22:18
  • I'm not trying to be unhelpful, just you current approach looks fraught with hidden dangers. I don't have a solution to your direct question, but there are other possible ways to do want you want to without resorting to stacktrace and reflection. I'll share one take if you want as an answer. – MrC aka Shaun Curtis Oct 13 '21 at 15:11
  • Related: [MoveNext instead of actual method name](https://stackoverflow.com/a/22599613/2791540) – John Wu Oct 13 '21 at 17:35

1 Answers1

1

Having initially thought it was a crazy idea, I was intrigued enough to explore how I would approach the problem if I had to. Here's a first demo hash up of a possible approach.

Define an Interface that all classes with Authorization need to implement.

using System;
using System.Collections.Generic;

namespace StackOverflow.Answers
{
    public interface IClassAuthorization
    {
        public List<string> UserRoles { get; set; }

        public Boolean UserHasRole(string role)
        {
            if (UserRoles is null)
                throw new ApplicationException("No UserRoles defined for object.  If you are using Class Authorization you must set the UserRoles");
            return UserRoles.Any(item => item.Equals(role, StringComparison.CurrentCultureIgnoreCase));
        }
    }
}

Define a result class. Not strictly required, you could return nulls for not implemented,... This just gives you a method to return both the result and data without resorting to outs. You can see all three implemented in the new WeatherForecastService.

namespace StackOverflow.Answers
{
    public class ClassAuthorizationResult
    {
        public ClassAuthorizationResultType Status { get; private set; } = ClassAuthorizationResultType.NotDefined;

        public object Data { get; set; } = null;

        public static ClassAuthorizationResult Success(object data)
            => new ClassAuthorizationResult { Status=ClassAuthorizationResultType.Success, Data = data };

        public static ClassAuthorizationResult Failure()
            => new ClassAuthorizationResult { Status = ClassAuthorizationResultType.Failure, Data = null };

        public static ClassAuthorizationResult NotDefined()
            => new ClassAuthorizationResult { Status = ClassAuthorizationResultType.NotDefined, Data = null };

        public enum ClassAuthorizationResultType
        {
            NotDefined,
            Success,
            Failure
        }
    }
}

Here's the new WeatherForecastService:

using StackOverflow.Answers.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace StackOverflow.Answers
{
    public class AuthorizedWeatherForecastService : IClassAuthorization
    {
        public List<string> UserRoles { get; set; } = null;

        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private IClassAuthorization _interface => this;

        private List<WeatherForecast> records;

        public AuthorizedWeatherForecastService()
        {
            records = GetWeatherForecasts;
        }

        //  Null return
        public Task<List<WeatherForecast>> ForecastsAsync()
            => Task.FromResult(_interface.UserHasRole("User")
                ? this.records
                : null);
        
        // ClassAuthorizationResult return
        public Task<ClassAuthorizationResult> GetForecastsAsync()
            => Task.FromResult(_interface.UserHasRole("User")
                ? ClassAuthorizationResult.Success(this.records)
                : ClassAuthorizationResult.Failure());

        // Out return
        public Task<bool> GetForecastsAsync(out List<WeatherForecast> list)
        {
            var ok = _interface.UserHasRole("User");
            list = ok ? this.records : new List<WeatherForecast>();
            return Task.FromResult(ok);
        }

        private List<WeatherForecast> GetWeatherForecasts
        {
            get
            {
                var rng = new Random();
                return Enumerable.Range(1, 5).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = rng.Next(-20, 55),
                    Summary = Summaries[rng.Next(Summaries.Length)]
                }).ToList();
            }
        }
    }
}

Then in FetchData

    [Inject] AuthorizedWeatherForecastService ForecastService { get; set; }

    protected override async Task OnInitializedAsync()
    {
        ForecastService.UserRoles = new List<string> { "User" };
        var result = await ForecastService.GetForecastsAsync();
        if (result.Status == ClassAuthorizationResult.ClassAuthorizationResultType.Success)
            forecasts = (List<WeatherForecast>)result.Data;
    }

You can get the user roles from AuthenticationState. If your classes are services, inject the IAuthenticationStateProvider directly and register for the AuthenticationStateChanged event to update the roles when the user changes.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Thank you for this. While it does not use the custom attribute class I was looking at, I think this is equally elegant. I will try to implement this this week! – jason Oct 13 '21 at 20:52