reduce code repeat, introduce MustSatisfyRequirementAuthorizationHandler

This commit is contained in:
Shannon
2020-11-26 17:25:43 +11:00
parent 75e6eb0bd9
commit aeab78f6a1
12 changed files with 127 additions and 170 deletions

View File

@@ -13,10 +13,11 @@ using Umbraco.Web.Editors;
namespace Umbraco.Web.BackOffice.Authorization
{
/// <summary>
/// if the users being edited is an admin then we must ensure that the current user is also an admin
/// </summary>
public class AdminUsersHandler : AuthorizationHandler<AdminUsersRequirement>
public class AdminUsersHandler : MustSatisfyRequirementAuthorizationHandler<AdminUsersRequirement>
{
private readonly IHttpContextAccessor _httpContextAcessor;
private readonly IUserService _userService;
@@ -34,14 +35,13 @@ namespace Umbraco.Web.BackOffice.Authorization
_userEditorAuthorizationHelper = userEditorAuthorizationHelper;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminUsersRequirement requirement)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, AdminUsersRequirement requirement)
{
var queryString = _httpContextAcessor.HttpContext?.Request.Query[requirement.QueryStringName];
if (!queryString.HasValue)
{
// must succeed this requirement since we cannot process it
context.Succeed(requirement);
return Task.CompletedTask;
return Task.FromResult(true);
}
int[] userIds;
@@ -55,8 +55,7 @@ namespace Umbraco.Web.BackOffice.Authorization
if (ids.Count == 0)
{
// must succeed this requirement since we cannot process it
context.Succeed(requirement);
return Task.CompletedTask;
return Task.FromResult(true);
}
userIds = ids.Select(x => x.Value.TryConvertTo<int>()).Where(x => x.Success).Select(x => x.Result).ToArray();
}
@@ -64,23 +63,13 @@ namespace Umbraco.Web.BackOffice.Authorization
if (userIds.Length == 0)
{
// must succeed this requirement since we cannot process it
context.Succeed(requirement);
return Task.CompletedTask;
return Task.FromResult(true);
}
var users = _userService.GetUsersById(userIds);
var isAuth = users.All(user => _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, null) != false);
if (isAuth)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
return Task.FromResult(isAuth);
}
}
}

View File

@@ -9,7 +9,7 @@ namespace Umbraco.Web.BackOffice.Authorization
/// <summary>
/// Ensures authorization is successful for a back office user.
/// </summary>
public class BackOfficeHandler : AuthorizationHandler<BackOfficeRequirement>
public class BackOfficeHandler : MustSatisfyRequirementAuthorizationHandler<BackOfficeRequirement>
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurity;
private readonly IRuntimeState _runtimeState;
@@ -20,34 +20,22 @@ namespace Umbraco.Web.BackOffice.Authorization
_runtimeState = runtimeState;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BackOfficeRequirement requirement)
{
if (!IsAuthorized(requirement))
{
context.Fail();
}
else
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
private bool IsAuthorized(BackOfficeRequirement requirement)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, BackOfficeRequirement requirement)
{
try
{
// if not configured (install or upgrade) then we can continue
// otherwise we need to ensure that a user is logged in
return _runtimeState.Level == RuntimeLevel.Install
var isAuth = _runtimeState.Level == RuntimeLevel.Install
|| _runtimeState.Level == RuntimeLevel.Upgrade
|| _backOfficeSecurity.BackOfficeSecurity?.ValidateCurrentUser(false, requirement.RequireApproval) == ValidateRequestAttempt.Success;
return Task.FromResult(isAuth);
}
catch (Exception)
{
return false;
return Task.FromResult(false);
}
}
}
}

View File

@@ -13,7 +13,7 @@ namespace Umbraco.Web.BackOffice.Authorization
/// <summary>
/// The user must have access to all descendant nodes of the content item in order to continue
/// </summary>
public class ContentPermissionsPublishBranchHandler : AuthorizationHandler<ContentPermissionsPublishBranchRequirement, IContent>
public class ContentPermissionsPublishBranchHandler : MustSatisfyRequirementAuthorizationHandler<ContentPermissionsPublishBranchRequirement, IContent>
{
private readonly IEntityService _entityService;
private readonly ContentPermissions _contentPermissions;
@@ -29,7 +29,7 @@ namespace Umbraco.Web.BackOffice.Authorization
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionsPublishBranchRequirement requirement, IContent resource)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsPublishBranchRequirement requirement, IContent resource)
{
var denied = new List<IUmbracoEntity>();
var page = 0;
@@ -55,16 +55,7 @@ namespace Umbraco.Web.BackOffice.Authorization
}
}
if (denied.Count == 0)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
return Task.FromResult(denied.Count == 0);
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Umbraco.Web.BackOffice.Authorization
/// <summary>
/// Used to authorize if the user has the correct permission access to the content for the content id specified in a query string
/// </summary>
public class ContentPermissionsQueryStringHandler : AuthorizationHandler<ContentPermissionsQueryStringRequirement>
public class ContentPermissionsQueryStringHandler : MustSatisfyRequirementAuthorizationHandler<ContentPermissionsQueryStringRequirement>
{
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IHttpContextAccessor _httpContextAccessor;
@@ -34,7 +34,7 @@ namespace Umbraco.Web.BackOffice.Authorization
_contentPermissions = contentPermissions;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionsQueryStringRequirement requirement)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsQueryStringRequirement requirement)
{
int nodeId;
if (requirement.NodeId.HasValue == false)
@@ -42,8 +42,7 @@ namespace Umbraco.Web.BackOffice.Authorization
if (!_httpContextAccessor.HttpContext.Request.Query.TryGetValue(requirement.QueryStringName, out var routeVal))
{
// must succeed this requirement since we cannot process it
context.Succeed(requirement);
return Task.CompletedTask;
return Task.FromResult(true);
}
else
{
@@ -72,29 +71,21 @@ namespace Umbraco.Web.BackOffice.Authorization
}
var permissionResult = _contentPermissions.CheckPermissions(nodeId,
_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser,
_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser,
out IContent contentItem,
new[] { requirement.PermissionToCheck });
switch (permissionResult)
{
case ContentPermissions.ContentAccess.Denied:
context.Fail();
break;
case ContentPermissions.ContentAccess.NotFound:
default:
context.Succeed(requirement);
break;
}
if (contentItem != null)
{
//store the content item in request cache so it can be resolved in the controller without re-looking it up
_httpContextAccessor.HttpContext.Items[typeof(IContent).ToString()] = contentItem;
}
return Task.CompletedTask;
return permissionResult switch
{
ContentPermissions.ContentAccess.Denied => Task.FromResult(false),
_ => Task.FromResult(true),
};
}
}
}

View File

@@ -9,7 +9,7 @@ namespace Umbraco.Web.BackOffice.Authorization
/// <summary>
/// Used to authorize if the user has the correct permission access to the content for the <see cref="IContent"/> specified
/// </summary>
public class ContentPermissionsResourceHandler : AuthorizationHandler<ContentPermissionsResourceRequirement, IContent>
public class ContentPermissionsResourceHandler : MustSatisfyRequirementAuthorizationHandler<ContentPermissionsResourceRequirement, IContent>
{
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly ContentPermissions _contentPermissions;
@@ -22,22 +22,13 @@ namespace Umbraco.Web.BackOffice.Authorization
_contentPermissions = contentPermissions;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionsResourceRequirement requirement, IContent resource)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsResourceRequirement requirement, IContent resource)
{
var permissionResult = _contentPermissions.CheckPermissions(resource,
_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser,
requirement.PermissionsToCheck);
if (permissionResult == ContentPermissions.ContentAccess.Denied)
{
context.Fail();
}
else
{
context.Succeed(requirement);
}
return Task.CompletedTask;
return Task.FromResult(permissionResult != ContentPermissions.ContentAccess.Denied);
}
}
}

View File

@@ -8,7 +8,7 @@ namespace Umbraco.Web.BackOffice.Authorization
/// <summary>
/// Ensures the resource cannot be accessed if <see cref="IBackOfficeExternalLoginProviders.HasDenyLocalLogin"/> returns true
/// </summary>
public class DenyLocalLoginHandler : AuthorizationHandler<DenyLocalLoginRequirement>
public class DenyLocalLoginHandler : MustSatisfyRequirementAuthorizationHandler<DenyLocalLoginRequirement>
{
private readonly IBackOfficeExternalLoginProviders _externalLogins;
@@ -17,18 +17,9 @@ namespace Umbraco.Web.BackOffice.Authorization
_externalLogins = externalLogins;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyLocalLoginRequirement requirement)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, DenyLocalLoginRequirement requirement)
{
if (_externalLogins.HasDenyLocalLogin())
{
context.Fail();
}
else
{
context.Succeed(requirement);
}
return Task.CompletedTask;
return Task.FromResult(!_externalLogins.HasDenyLocalLogin());
}
}
}

View File

@@ -10,7 +10,7 @@ using Umbraco.Core.Services;
namespace Umbraco.Web.BackOffice.Authorization
{
public class MediaPermissionsQueryStringHandler : AuthorizationHandler<MediaPermissionsQueryStringRequirement>
public class MediaPermissionsQueryStringHandler : MustSatisfyRequirementAuthorizationHandler<MediaPermissionsQueryStringRequirement>
{
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IHttpContextAccessor _httpContextAccessor;
@@ -29,13 +29,12 @@ namespace Umbraco.Web.BackOffice.Authorization
_mediaPermissions = mediaPermissions;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MediaPermissionsQueryStringRequirement requirement)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, MediaPermissionsQueryStringRequirement requirement)
{
if (!_httpContextAccessor.HttpContext.Request.Query.TryGetValue(requirement.QueryStringName, out var routeVal))
{
// must succeed this requirement since we cannot process it
context.Succeed(requirement);
return Task.CompletedTask;
return Task.FromResult(true);
}
int nodeId;
@@ -63,24 +62,17 @@ namespace Umbraco.Web.BackOffice.Authorization
nodeId,
out var mediaItem);
switch (permissionResult)
{
case MediaPermissions.MediaAccess.Denied:
context.Fail();
break;
case MediaPermissions.MediaAccess.NotFound:
default:
context.Succeed(requirement);
break;
}
if (mediaItem != null)
{
//store the content item in request cache so it can be resolved in the controller without re-looking it up
_httpContextAccessor.HttpContext.Items[typeof(IMedia).ToString()] = mediaItem;
}
return Task.CompletedTask;
return permissionResult switch
{
MediaPermissions.MediaAccess.Denied => Task.FromResult(false),
_ => Task.FromResult(true),
};
}
}
}

View File

@@ -8,7 +8,7 @@ namespace Umbraco.Web.BackOffice.Authorization
/// <summary>
/// Used to authorize if the user has the correct permission access to the content for the <see cref="IContent"/> specified
/// </summary>
public class MediaPermissionsResourceHandler : AuthorizationHandler<MediaPermissionsResourceRequirement, IMedia>
public class MediaPermissionsResourceHandler : MustSatisfyRequirementAuthorizationHandler<MediaPermissionsResourceRequirement, IMedia>
{
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly MediaPermissions _mediaPermissions;
@@ -21,7 +21,7 @@ namespace Umbraco.Web.BackOffice.Authorization
_mediaPermissions = mediaPermissions;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MediaPermissionsResourceRequirement requirement, IMedia resource)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, MediaPermissionsResourceRequirement requirement, IMedia resource)
{
var permissionResult = MediaPermissions.MediaAccess.NotFound;
@@ -39,16 +39,7 @@ namespace Umbraco.Web.BackOffice.Authorization
out _);
}
if (permissionResult == MediaPermissions.MediaAccess.Denied)
{
context.Fail();
}
else
{
context.Succeed(requirement);
}
return Task.CompletedTask;
return Task.FromResult(permissionResult != MediaPermissions.MediaAccess.Denied);
}
}
}

View File

@@ -0,0 +1,70 @@
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
namespace Umbraco.Web.BackOffice.Authorization
{
/// <summary>
/// Abstract handler that must satisfy the requirement so Succeed or Fail will be called no matter what
/// </summary>
/// <typeparam name="T"></typeparam>
/// <remarks>
/// aspnetcore Authz handlers are not required to satisfy the requirement and generally don't explicitly call Fail when the requirement
/// isn't satisfied, however in many simple cases explicitly calling Succeed or Fail is what we want which is what this class is used for.
/// </remarks>
public abstract class MustSatisfyRequirementAuthorizationHandler<T> : AuthorizationHandler<T> where T : IAuthorizationRequirement
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement)
{
var isAuth = await IsAuthorized(context, requirement);
if (isAuth)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
/// <summary>
/// Return true if the requirement is succeeded or ignored, return false if the requirement is explicitly not met
/// </summary>
/// <param name="context"></param>
/// <param name="requirement"></param>
/// <returns></returns>
protected abstract Task<bool> IsAuthorized(AuthorizationHandlerContext context, T requirement);
}
/// <summary>
/// Abstract handler that must satisfy the requirement so Succeed or Fail will be called no matter what
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TResource"></typeparam>
/// <remarks>
/// aspnetcore Authz handlers are not required to satisfy the requirement and generally don't explicitly call Fail when the requirement
/// isn't satisfied, however in many simple cases explicitly calling Succeed or Fail is what we want which is what this class is used for.
/// </remarks>
public abstract class MustSatisfyRequirementAuthorizationHandler<T, TResource> : AuthorizationHandler<T, TResource> where T : IAuthorizationRequirement
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement, TResource resource)
{
var isAuth = await IsAuthorized(context, requirement, resource);
if (isAuth)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
/// <summary>
/// Return true if the requirement is succeeded or ignored, return false if the requirement is explicitly not met
/// </summary>
/// <param name="context"></param>
/// <param name="requirement"></param>
/// <returns></returns>
protected abstract Task<bool> IsAuthorized(AuthorizationHandlerContext context, T requirement, TResource resource);
}
}

View File

@@ -12,7 +12,7 @@ namespace Umbraco.Web.BackOffice.Authorization
/// <remarks>
/// The user only needs access to one of the sections specified, not all of the sections.
/// </remarks>
public class SectionHandler : AuthorizationHandler<SectionRequirement>
public class SectionHandler : MustSatisfyRequirementAuthorizationHandler<SectionRequirement>
{
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
@@ -21,27 +21,13 @@ namespace Umbraco.Web.BackOffice.Authorization
_backofficeSecurityAccessor = backofficeSecurityAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SectionRequirement requirement)
{
if (IsAuthorized(requirement))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
private bool IsAuthorized(SectionRequirement requirement)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, SectionRequirement requirement)
{
var authorized = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null
&& requirement.SectionAliases.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess(
app, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser));
return authorized;
return Task.FromResult(authorized);
}
}
}

View File

@@ -16,7 +16,7 @@ namespace Umbraco.Web.BackOffice.Authorization
/// This would allow a tree to be moved between sections.
/// The user only needs access to one of the trees specified, not all of the trees.
/// </remarks>
public class TreeHandler : AuthorizationHandler<TreeRequirement>
public class TreeHandler : MustSatisfyRequirementAuthorizationHandler<TreeRequirement>
{
private readonly ITreeService _treeService;
@@ -37,21 +37,7 @@ namespace Umbraco.Web.BackOffice.Authorization
_backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor));
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TreeRequirement requirement)
{
if (IsAuthorized(requirement))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
private bool IsAuthorized(TreeRequirement requirement)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, TreeRequirement requirement)
{
var apps = requirement.TreeAliases.Select(x => _treeService
.GetByAlias(x))
@@ -60,9 +46,12 @@ namespace Umbraco.Web.BackOffice.Authorization
.Distinct()
.ToArray();
return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null
var isAuth = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null
&& apps.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess(
app, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser));
return Task.FromResult(isAuth);
}
}
}

View File

@@ -12,7 +12,7 @@ namespace Umbraco.Web.BackOffice.Authorization
/// <summary>
/// Authorizes that the current user has access to the user group Id in the request
/// </summary>
public class UserGroupHandler : AuthorizationHandler<UserGroupRequirement>
public class UserGroupHandler : MustSatisfyRequirementAuthorizationHandler<UserGroupRequirement>
{
private readonly IHttpContextAccessor _httpContextAcessor;
private readonly IUserService _userService;
@@ -36,22 +36,7 @@ namespace Umbraco.Web.BackOffice.Authorization
_backofficeSecurityAccessor = backofficeSecurityAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserGroupRequirement requirement)
{
var isAuth = IsAuthorized(requirement);
if (!isAuth.HasValue || isAuth.Value)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
private bool? IsAuthorized(UserGroupRequirement requirement)
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, UserGroupRequirement requirement)
{
var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
@@ -69,8 +54,11 @@ namespace Umbraco.Web.BackOffice.Authorization
_contentService,
_mediaService,
_entityService);
return authHelper.AuthorizeGroupAccess(currentUser, intIds);
var isAuth = authHelper.AuthorizeGroupAccess(currentUser, intIds);
return Task.FromResult(isAuth.Success);
}
}
}