ユーザークレームとアクセスされるリソースに基づいて承認される必要があるいくつかのコントローラーとアクションメソッドを備えたdotnet core 2.2 apiがあります。基本的に、各ユーザーは各リソースに対して0個または多くの「ロール」を持つことができます。これはすべてASP.NET Identity Claimsを使用して行われます。
だから、私の理解は Resource-based authorization を利用する必要があるということです。しかし、どちらの例もほとんど同じであり、各アクションメソッドに明示的な命令のif/elseロジックが必要です。これは、回避しようとしていることです。
次のようなことができるようになりたい
[Authorize("Admin")] // or something similar
public async Task<IActionResult> GetSomething(int resourceId)
{
var resource = await SomeRepository.Get(resourceId);
return Json(resource);
}
そして、どこか他の場所でポリシー/フィルター/要件/何でも承認ロジックを定義し、現在のユーザーのクレームとエンドポイントが受け取ったresourceId
パラメーターの両方にアクセスできます。そのため、ユーザーが特定のresourceId
に対して「管理者」の役割を持っていることを示すクレームがあるかどうかを確認できます。
編集:フィードバックに基づいて動的にします
RBACと.NETのクレームで重要なことは、ClaimsIdentityを作成し、フレームワークにそれを任せることです。以下は、クエリパラメータ「user」を調べ、辞書に基づいてClaimsPrincipalを生成するミドルウェアの例です。
実際にIDプロバイダーに接続する必要を回避するために、ClaimsPrincipalをセットアップするミドルウェアを作成しました。
// **THIS CLASS IS ONLY TO DEMONSTRATE HOW THE ROLES NEED TO BE SETUP **
public class CreateFakeIdentityMiddleware
{
private readonly RequestDelegate _next;
public CreateFakeIdentityMiddleware(RequestDelegate next)
{
_next = next;
}
private readonly Dictionary<string, string[]> _tenantRoles = new Dictionary<string, string[]>
{
["tenant1"] = new string[] { "Admin", "Reader" },
["tenant2"] = new string[] { "Reader" },
};
public async Task InvokeAsync(HttpContext context)
{
// Assume this is the roles
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "John"),
new Claim(ClaimTypes.Email, "[email protected]")
};
foreach (KeyValuePair<string, string[]> tenantRole in _tenantRoles)
{
claims.AddRange(tenantRole.Value.Select(x => new Claim(ClaimTypes.Role, $"{tenantRole.Key}:{x}".ToLower())));
}
// Note: You need these for the AuthorizeAttribute.Roles
claims.AddRange(_tenantRoles.SelectMany(x => x.Value)
.Select(x => new Claim(ClaimTypes.Role, x.ToLower())));
context.User = new System.Security.Claims.ClaimsPrincipal(new ClaimsIdentity(claims,
"Bearer"));
await _next(context);
}
}
これを結び付けるには、スタートアップクラスで IApplicationBuilder に seMiddleware 拡張メソッドを使用するだけです。
app.UseMiddleware<RBACExampleMiddleware>();
クエリパラメーター「テナント」を検索し、ロールに基づいて成功または失敗するAuthorizationHandlerを作成します。
public class SetTenantIdentityHandler : AuthorizationHandler<TenantRoleRequirement>
{
public const string TENANT_KEY_QUERY_NAME = "tenant";
private static readonly ConcurrentDictionary<string, string[]> _methodRoles = new ConcurrentDictionary<string, string[]>();
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TenantRoleRequirement requirement)
{
if (HasRoleInTenant(context))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
private bool HasRoleInTenant(AuthorizationHandlerContext context)
{
if (context.Resource is AuthorizationFilterContext authorizationFilterContext)
{
if (authorizationFilterContext.HttpContext
.Request
.Query
.TryGetValue(TENANT_KEY_QUERY_NAME, out StringValues tenant)
&& !string.IsNullOrWhiteSpace(tenant))
{
if (TryGetRoles(authorizationFilterContext, tenant.ToString().ToLower(), out string[] roles))
{
if (context.User.HasClaim(x => roles.Any(r => x.Value == r)))
{
return true;
}
}
}
}
return false;
}
private bool TryGetRoles(AuthorizationFilterContext authorizationFilterContext,
string tenantId,
out string[] roles)
{
string actionId = authorizationFilterContext.ActionDescriptor.Id;
roles = null;
if (!_methodRoles.TryGetValue(actionId, out roles))
{
roles = authorizationFilterContext.Filters
.Where(x => x.GetType() == typeof(AuthorizeFilter))
.Select(x => x as AuthorizeFilter)
.Where(x => x != null)
.Select(x => x.Policy)
.SelectMany(x => x.Requirements)
.Where(x => x.GetType() == typeof(RolesAuthorizationRequirement))
.Select(x => x as RolesAuthorizationRequirement)
.SelectMany(x => x.AllowedRoles)
.ToArray();
_methodRoles.TryAdd(actionId, roles);
}
roles = roles?.Select(x => $"{tenantId}:{x}".ToLower())
.ToArray();
return roles != null;
}
}
TenantRoleRequirementは非常に単純なクラスです。
public class TenantRoleRequirement : IAuthorizationRequirement { }
次に、startup.csファイルに次のようにすべてを接続します。
services.AddTransient<IAuthorizationHandler, SetTenantIdentityHandler>();
// Although this isn't used to generate the identity, it is needed
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Audience = "https://localhost:5000/";
options.Authority = "https://localhost:5000/identity/";
});
services.AddAuthorization(authConfig =>
{
authConfig.AddPolicy(Policies.HasRoleInTenant, policyBuilder => {
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(new TenantRoleRequirement());
});
});
メソッドは次のようになります。
// TOOD: Move roles to a constants/globals
[Authorize(Policy = Policies.HasRoleInTenant, Roles = "admin")]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
以下はテストシナリオです:
このアプローチの重要な点は、私が実際に403を返すことは決してないということです。コードはIDを設定し、フレームワークに結果を処理させます。これにより、認証と承認が分離されます。
〜乾杯
コメントに基づいて編集
私の理解によると、現在のユーザー(それに関連するすべての情報)、コントローラー(またはアクション)に指定する役割、およびエンドポイントが受信するパラメーターにアクセスする必要があります。 Web APIは試していませんが、asp.netコアMVCの場合は、ポリシーベースの承認でAuthorizationHandler
を使用してこれを実現し、ロール-リソースアクセスを決定するために特別に作成された挿入サービスと組み合わせることができます。
これを行うには、まず_Startup.ConfigureServices
_でポリシーを設定します。
_services.AddAuthorization(options =>
{
options.AddPolicy("UserResource", policy => policy.Requirements.Add( new UserResourceRequirement() ));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
services.AddScoped<IRoleResourceService, RoleResourceService>();
_
次にUserResourceHandler
を作成します。
_public class UserResourceHandler : AuthorizationHandler<UserResourceRequirement>
{
readonly IRoleResourceService _roleResourceService;
public UserResourceHandler (IRoleResourceService r)
{
_roleResourceService = r;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, UserResourceRequirement requirement)
{
if (context.Resource is AuthorizationFilterContext filterContext)
{
var area = (filterContext.RouteData.Values["area"] as string)?.ToLower();
var controller = (filterContext.RouteData.Values["controller"] as string)?.ToLower();
var action = (filterContext.RouteData.Values["action"] as string)?.ToLower();
var id = (filterContext.RouteData.Values["id"] as string)?.ToLower();
if (_roleResourceService.IsAuthorize(area, controller, action, id))
{
context.Succeed(requirement);
}
}
}
}
_
エンドポイントが受け取ったパラメーターにアクセスするには、_context.Resource
_をAuthorizationFilterContext
にキャストすることで、RouteData
にアクセスできるようにします。 UserResourceRequirement
については、空のままにすることができます。
_public class UserResourceRequirement : IAuthorizationRequirement { }
_
IRoleResourceService
については、何でも注入できるようにするための単純なサービスクラスです。このサービスは、アクションの属性でロールを指定する必要がないように、コードでロールをアクションにペアリングする代用です。そうすることで、実装を自由に選択できます。たとえば、データベースから、構成ファイルから、またはハードコードから。
RoleResourceService
のユーザーにアクセスするには、IHttpContextAccessor
を挿入します。 IHttpContextAccessor
を注入可能にするには、_Startup.ConfigurationServices
_メソッド本体にservices.AddHttpContextAccessor()
を追加することに注意してください。
以下は設定ファイルから情報を取得する例です:
_public class RoleResourceService : IRoleResourceService
{
readonly IConfiguration _config;
readonly IHttpContextAccessor _accessor;
readonly UserManager<AppUser> _userManager;
public class RoleResourceService(IConfiguration c, IHttpContextAccessor a, UserManager<AppUser> u)
{
_config = c;
_accessor = a;
_userManager = u;
}
public bool IsAuthorize(string area, string controller, string action, string id)
{
var roleConfig = _config.GetValue<string>($"RoleSetting:{area}:{controller}:{action}"); //assuming we have the setting in appsettings.json
var appUser = await _userManager.GetUserAsync(_accessor.HttpContext.User);
var userRoles = await _userManager.GetRolesAsync(appUser);
// all of needed data are available now, do the logic of authorization
return result;
}
}
_
データベースから設定を取得することは確かにもう少し複雑ですが、AppDbContext
を注入できるため、実行できます。ハードコード化されたアプローチについては、それを行うための多くの方法が存在します。
すべてが完了したら、アクションにポリシーを使用します。
_[Authorize(Policy = "UserResource")] //dont need Role name because of the RoleResourceService
public ActionResult<IActionResult> GetSomething(int resourceId)
{
//existing code
}
_
実際、適用したいあらゆるアクションに「UserResource」ポリシーを使用できます。