casbin-net / casbin-aspnetcore

Casbin.NET integration middleware and sample code for ASP.NET Core
https://github.com/casbin/Casbin.NET
Apache License 2.0
64 stars 20 forks source link

[Question] How to retrieve NetCasbin.Enforcer from ApiController? #36

Closed VerdonTrigance closed 2 years ago

VerdonTrigance commented 2 years ago

Hi guys.

I have this at startup:

public static void AddAclAuthorization(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddDbContext<CasbinDbContext>(options => options.UseNpgsql(
                configuration.GetConnectionString("Casbin"),
                options => options.MigrationsHistoryTable("__CasbinMigrationsHistory")
                    .MigrationsAssembly("MyComp.Casbin")));

            services.AddCasbinAuthorization(options =>
            {
                options.PreferSubClaimType = OpenIDStandardClaims.PreferredUsername;
                options.DefaultModelPath = Path.Combine("Authorization", "Models", configuration["Authorization:Casbin:ModelName"]);
                options.DefaultEnforcerFactory = (p, m) =>
                    new Enforcer(m, new EFCoreAdapter(p.GetRequiredService<CasbinDbContext>()));
            });
        }

And looking for a way to retrieve all available business objects for current user in a ApiController. For doing this I assume to call NetCasbin.Enforcer.GetPermissionsForUser() in my controller, filter 'this and that' and return result to requestor. Actually I didn't find AddCasbinAuthorization() method in the repo and now looking for a way to get Enforcer or something else to make that call and adding DI to ApiController constructor:

public MyController(ILogger<MyController> logger, DbCtx ctx, IConfiguration configuration, IEnforcer enforcer) // what to inject here?
        {
            _logger = logger;
            _dbContext = ctx;
            Configuration = configuration;
            Enforcer = enforcer;
        }

I'm new to Casbin and not sure what to inject?

casbin-bot commented 2 years ago

@sagilio @xcaptain @huazhikui

hsluoyz commented 2 years ago

@sagilio

VerdonTrigance commented 2 years ago

Oh, I was looking in a wrong place. I found this:

public static IServiceCollection AddCasbinAuthorizationCore(
            this IServiceCollection services,
            Action<CasbinAuthorizationOptions>? configureOptions = default,
            ServiceLifetime defaultEnforcerProviderLifeTime = ServiceLifetime.Scoped,
            ServiceLifetime defaultModelProviderLifeTime = ServiceLifetime.Scoped)
        {
            services.AddCasbin(configureOptions, defaultEnforcerProviderLifeTime, defaultModelProviderLifeTime);
            services.TryAddSingleton<ICasbinAuthorizationContextFactory, DefaultCasbinAuthorizationContextFactory>();
            services.TryAddScoped<IEnforceService, DefaultEnforcerService>();
            services.TryAddSingleton<IRequestTransformersCache, RequestTransformersCache>();

            // Can not change to TryAdd, because there interface may need more than one implement.
            services.AddScoped<IAuthorizationHandler, CasbinAuthorizationHandler>();
            services.AddSingleton<IRequestTransformer, BasicRequestTransformer>();
            services.AddSingleton<IRequestTransformer, RbacRequestTransformer>();
            services.AddSingleton<IRequestTransformer, KeyMatchRequestTransformer>();

            services.AddAuthorizationCore();
            return services;
        }

and was trying to do this:

[HttpGet]
        [ProducesResponseType(StatusCodes.Status200OK)]
        public async Task<IActionResult> GetAll([FromQuery] int? pageNumber, [FromServices] IEnforceService enforceService
            , [FromServices] ICasbinAuthorizationContext casbinCtx)
        {
            var currentSubject = casbinCtx.AuthorizationData.First().Value1;
            var availableObjects = _casbinDbCtx.CasbinRules
                .Where(p => p.V0 == currentSubject)
                .Select(p => p.V1);
// other stuff
 }

But application says "ICasbinAuthorizationContext not registered" and it's actually not. As we can see there is ICasbinAuthorizationContextFactory registering, not ICasbinAuthorizationContext.

As workaround I might use HttpContext and look for claim that was defined like identifier here in the options:

options.PreferSubClaimType = OpenIDStandardClaims.PreferredUsername;

But it's not best way I guess.

sagilio commented 2 years ago

You can inject IEnforcerProvider here, and use GetEnforcer to get the instance. If you want to get the current user, you can only use the User property in the controller. I think It is better to provide an extension method to get the correct sub value.

VerdonTrigance commented 2 years ago

Injecting IEnforcerProvider works, but when it registered? I didn't find it neither in AddCasbinAuthorization() nor AddCasbinAuthorizationCore().

sagilio commented 2 years ago

@VerdonTrigance https://github.com/casbin-net/casbin-aspnetcore/blob/0cb68a5b5ebf20424d60978f467ace325fe34309/src/Casbin.AspNetCore.Core/CoreServiceCollectionExtension.cs#L20-L33

VerdonTrigance commented 2 years ago

I ended with this approach:

public MyController(ILogger<MyController> logger, MyDbCtx ctx, IConfiguration configuration,
            CasbinDbContext casbinDbCtx, IOptions<CasbinAuthorizationOptions> casbinOptions)
        {
            _logger = logger;
            _dbContext = ctx;
            Configuration = configuration;
            _casbinDbCtx = casbinDbCtx;
            _casbinOptions = casbinOptions?.Value;
        }
public async Task<IActionResult> GetAll([FromServices] IEnforcerProvider enforcerProvider)
        {
            var currentSubject = User.FindFirst(_casbinOptions?.PreferSubClaimType)?.Value;
            var enforcer = enforcerProvider.GetEnforcer();
            var perms = enforcer.GetPermissionsForUser(currentSubject);
            // other stuff
}
VerdonTrigance commented 2 years ago

Khm... I've got exception "{"Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.\r\nObject name: 'CasbinDbContext'."}" when trying to AddPolicy here in my controller method:

public async Task<IActionResult> CreateObject([FromBody] NewObjectRequest data, [FromServices] IEnforcerProvider enforcerProvider)
        {
// other stuff
var enforcer = enforcerProvider.GetEnforcer();
                bool success = enforcer.AddPermissionForUser(currentSubject, actionPath, HttpMethods.Get.ToUpper());
}

My Casbin registration is:

public static void AddAclAuthorization(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddDbContext<CasbinDbContext>(options => options.UseNpgsql(
                configuration.GetConnectionString("Casbin"),
                options => options.MigrationsHistoryTable("__CasbinMigrationsHistory")
                    .MigrationsAssembly("Org.Proj.Casbin")));

            services.AddCasbinAuthorization(options =>
            {
                options.DefaultModelPath = Path.Combine("Authorization", "Models", configuration["Authorization:Casbin:ModelName"]);
                options.DefaultEnforcerFactory = (p, m) =>
                    new Enforcer(m, new EFCoreAdapter(p.GetRequiredService<CasbinDbContext>()));
                options.DefaultAuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme;
                options.PreferSubClaimType = KeycloakCustomClaims.Email2;
                options.AllowAnyone = true;
            });
        }

Why it's disposed? Lifetime of any DbContext is Transient. But lifetime of IEnforcerProvider is Scope. I even tried to change lifetime of my CasbinDbContext to Scope like this:

services.AddDbContext<CasbinDbContext>(options => options.UseNpgsql(
                configuration.GetConnectionString("Casbin"),
                options => options.MigrationsHistoryTable("__CasbinMigrationsHistory")
                    .MigrationsAssembly("Org.Proj.Casbin")));
                ServiceLifetime.Scoped);

But nothing changed. So where it's been disposing?

As workaround I may get CasbinDbContext and manually create CasbinRule object. But it's not good.

hsluoyz commented 2 years ago

@sagilio