Compare commits

...
9 changed files with 430 additions and 0 deletions
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Validation.AspNetCore;
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
using Umbraco.Cms.Api.Management.Security.Authorization.Contextual;
using Umbraco.Cms.Api.Management.Security.Authorization.DenyLocalLogin;
using Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
using Umbraco.Cms.Api.Management.Security.Authorization.Media;
@@ -30,6 +31,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
builder.Services.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, AllowedApplicationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, BackOfficeHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, ContextualPermissionHandler>();
builder.Services.AddAuthorization(CreatePolicies);
return builder;
@@ -139,5 +141,11 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
policy.Requirements.Add(new UserPermissionRequirement());
});
options.AddPolicy(ContextualPermissionHandler.ContextualPermissionsPolicyAlias, policy =>
{
policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
policy.Requirements.Add(new ContextualPermissionRequirement());
});
}
}
@@ -0,0 +1,70 @@
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Security.Authorization.Contextual;
/// <summary>
/// Authorizes a certain permission (read,write,browse,...) within a given context (umbraco, my-package,...)
/// against the <see cref="IGranularPermission"> granular permissions</see> defined on all <see cref="IUserGroup">user groups</see> the <see cref="IUser">user</see> is part off.
/// This is accomplished by validating the <see cref="ContextualPermissionHandler.ContextualPermissionsPolicyAlias">Contextual Permissions Policy</see> trough the <see cref="IAuthorizationService"/>
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ContextualAuthorizeAttribute : Attribute, IAsyncAuthorizationFilter
{
public ContextualAuthorizeAttribute(string permission, string context)
{
Permissions = permission.Yield();
Context = context;
PermissionMatchingBehaviour = PermissionMatchingBehaviour.All;
PermissionScopeMatchingBehaviour = PermissionScopeMatchingBehaviour.Any;
}
public ContextualAuthorizeAttribute(
IEnumerable<string> permissions,
string context,
PermissionMatchingBehaviour permissionMatchingBehaviour,
PermissionScopeMatchingBehaviour permissionScopeMatchingBehaviour)
{
Permissions = permissions;
Context = context;
PermissionMatchingBehaviour = permissionMatchingBehaviour;
PermissionScopeMatchingBehaviour = permissionScopeMatchingBehaviour;
}
private IEnumerable<string> Permissions { get; }
private string Context { get; }
private PermissionMatchingBehaviour PermissionMatchingBehaviour { get; }
private PermissionScopeMatchingBehaviour PermissionScopeMatchingBehaviour { get; }
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
IAuthorizationService authorizationService = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
AuthorizationResult authorizationResult = await authorizationService.AuthorizeResourceAsync(
context.HttpContext.User,
ContextualPermissionResource.WithSetup(Permissions, Context, PermissionMatchingBehaviour, PermissionScopeMatchingBehaviour),
ContextualPermissionHandler.ContextualPermissionsPolicyAlias);
if (authorizationResult.Succeeded is false)
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.HttpContext.Response.HttpContext.Features.Get<IHttpResponseFeature>()!.ReasonPhrase =
"Invalid contextual permission";
context.Result = new JsonResult("Invalid contextual permission")
{
Value = new { Status = "Unauthorized", Message = "Invalid contextual permission" },
};
}
}
}
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
using Umbraco.Cms.Core.Security.Authorization;
namespace Umbraco.Cms.Api.Management.Security.Authorization.Contextual;
/// Authorizes a certain permission (read,write,browse,...) within a given context (umbraco, my-package,...)
/// against the <see cref="IGranularPermission">granular permissions</see> defined on all <see cref="IUserGroup">user groups</see> the <see cref="IUser">user</see> is part off.
/// Permissions and Context are checked on a InvariantCultureIgnoreCase basis.
public class ContextualPermissionHandler : MustSatisfyRequirementAuthorizationHandler<ContextualPermissionRequirement,
ContextualPermissionResource>
{
public const string ContextualPermissionsPolicyAlias = "Umbraco.ContextualPermissions";
private readonly IAuthorizationHelper _authorizationHelper;
private readonly IContextualPermissionAuthorizer _contextualPermissionAuthorizer;
public ContextualPermissionHandler(
IAuthorizationHelper authorizationHelper,
IContextualPermissionAuthorizer contextualPermissionAuthorizer)
{
_authorizationHelper = authorizationHelper;
_contextualPermissionAuthorizer = contextualPermissionAuthorizer;
}
protected override async Task<bool> IsAuthorized(
AuthorizationHandlerContext context,
ContextualPermissionRequirement requirement,
ContextualPermissionResource resource)
{
IUser user = _authorizationHelper.GetUmbracoUser(context.User);
return _contextualPermissionAuthorizer.IsDenied(user, resource) is false;
}
}
@@ -0,0 +1,7 @@
using Microsoft.AspNetCore.Authorization;
namespace Umbraco.Cms.Api.Management.Security.Authorization.Contextual;
public class ContextualPermissionRequirement : IAuthorizationRequirement
{
}
@@ -427,6 +427,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddSingleton<IMediaPermissionAuthorizer, MediaPermissionAuthorizer>();
Services.AddSingleton<IUserGroupPermissionAuthorizer, UserGroupPermissionAuthorizer>();
Services.AddSingleton<IUserPermissionAuthorizer, UserPermissionAuthorizer>();
Services.AddSingleton<IContextualPermissionAuthorizer, ContextualPermissionAuthorizer>();
// Segments
Services.AddUnique<ISegmentService, NoopSegmentService>();
@@ -0,0 +1,21 @@
using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Core.Security.Authorization;
public class ContextualPermissionAuthorizer : IContextualPermissionAuthorizer
{
public bool IsDenied(IUser currentUser, ContextualPermissionResource resource)
{
return resource.PermissionMatchingBehaviour == PermissionMatchingBehaviour.Any
? !resource.Permissions.Any(ContextualMatch)
: !resource.Permissions.All(ContextualMatch);
bool ContextualMatch(string permissionToCheck) =>
currentUser.Groups.SelectMany(g => g.GranularPermissions)
.Any(definedContextualPermission =>
(resource.PermissionScopeMatchingBehaviour == PermissionScopeMatchingBehaviour.Any
|| (resource.PermissionScopeMatchingBehaviour == PermissionScopeMatchingBehaviour.ContextWideOnly && definedContextualPermission.Key is null))
&& definedContextualPermission.Context.Equals(resource.Context, StringComparison.InvariantCultureIgnoreCase)
&& definedContextualPermission.Permission.Equals(permissionToCheck, StringComparison.InvariantCultureIgnoreCase));
}
}
@@ -0,0 +1,70 @@
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Security.Authorization;
public class ContextualPermissionResource : IPermissionResource
{
public static ContextualPermissionResource WithPermission(string permission, string context) =>
WithAllPermissions(permission.Yield(), context);
public static ContextualPermissionResource WithContextWidePermission(string permission, string context) =>
WithAllContextWidePermissions(permission.Yield(), context);
public static ContextualPermissionResource WithAnyContextWidePermissions(IEnumerable<string> permissions, string context) =>
new(permissions, context, PermissionMatchingBehaviour.Any, PermissionScopeMatchingBehaviour.ContextWideOnly);
public static ContextualPermissionResource WithAllContextWidePermissions(IEnumerable<string> permissions, string context) =>
new(permissions, context, PermissionMatchingBehaviour.All, PermissionScopeMatchingBehaviour.ContextWideOnly);
public static ContextualPermissionResource WithAnyPermissions(IEnumerable<string> permissions, string context) =>
new(permissions, context, PermissionMatchingBehaviour.Any, PermissionScopeMatchingBehaviour.Any);
public static ContextualPermissionResource WithAllPermissions(IEnumerable<string> permissions, string context) =>
new(permissions, context, PermissionMatchingBehaviour.All, PermissionScopeMatchingBehaviour.Any);
public static ContextualPermissionResource WithSetup(
IEnumerable<string> permissions,
string context,
PermissionMatchingBehaviour permissionMatchingBehaviour,
PermissionScopeMatchingBehaviour permissionScopeMatchingBehaviour) =>
new(permissions, context, permissionMatchingBehaviour, permissionScopeMatchingBehaviour);
private ContextualPermissionResource(
IEnumerable<string> permissions,
string context,
PermissionMatchingBehaviour permissionMatchingBehaviour,
PermissionScopeMatchingBehaviour permissionScopeMatchingBehaviour)
{
Permissions = permissions;
Context = context;
PermissionMatchingBehaviour = permissionMatchingBehaviour;
PermissionScopeMatchingBehaviour = permissionScopeMatchingBehaviour;
}
public IEnumerable<string> Permissions { get; }
public string Context { get; }
public PermissionMatchingBehaviour PermissionMatchingBehaviour { get; }
public PermissionScopeMatchingBehaviour PermissionScopeMatchingBehaviour { get; }
}
public enum PermissionMatchingBehaviour
{
Any,
All,
}
public enum PermissionScopeMatchingBehaviour
{
/// <summary>
/// Both context wide and granular
/// </summary>
Any,
/// <summary>
/// Only Context wide
/// </summary>
ContextWideOnly,
}
@@ -0,0 +1,8 @@
using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Core.Security.Authorization;
public interface IContextualPermissionAuthorizer
{
bool IsDenied(IUser currentUser, ContextualPermissionResource resource);
}
@@ -0,0 +1,210 @@
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
using Umbraco.Cms.Core.Security.Authorization;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Security.Authorization;
[TestFixture]
public class ContextualPermissionAuthorizerTests
{
private const string FirstContext = "firstContext";
private const string SecondContext = "secondContext";
private const string FirstPermission = "firstPermission";
private const string SecondPermission = "secondPermission";
private const string ThirdPermission = "thirdPermission";
[Test]
public void IsDenied_True_NoGranularPermissions()
{
// arrange
var user = BuildUser(new HashSet<IGranularPermission>());
var resource = ContextualPermissionResource.WithPermission(FirstPermission, FirstContext);
var sut = new ContextualPermissionAuthorizer();
// act
var isDenied = sut.IsDenied(user, resource);
// assert
Assert.IsTrue(isDenied);
}
[Test]
public void IsDenied_True_IncorrectGranularPermission()
{
// arrange
var user = BuildUser(new HashSet<IGranularPermission>
{
new UnknownTypeGranularPermission { Context = FirstContext, Permission = SecondPermission },
});
var resource = ContextualPermissionResource.WithPermission(FirstPermission, FirstContext);
var sut = new ContextualPermissionAuthorizer();
// act
var isDenied = sut.IsDenied(user, resource);
// assert
Assert.IsTrue(isDenied);
}
[Test]
public void IsDenied_True_IncorrectGranularPermissions()
{
// arrange
var user = BuildUser(new HashSet<IGranularPermission>
{
new UnknownTypeGranularPermission { Context = FirstContext, Permission = ThirdPermission },
new UnknownTypeGranularPermission { Context = SecondContext, Permission = FirstPermission },
});
var resource =
ContextualPermissionResource.WithAnyPermissions([FirstPermission, SecondPermission], FirstContext);
var sut = new ContextualPermissionAuthorizer();
// act
var isDenied = sut.IsDenied(user, resource);
// assert
Assert.IsTrue(isDenied);
}
[Test]
public void IsDenied_True_NotEnoughGranularPermissions()
{
// arrange
var user = BuildUser(new HashSet<IGranularPermission>
{
new UnknownTypeGranularPermission { Context = FirstContext, Permission = SecondPermission },
new UnknownTypeGranularPermission { Context = FirstContext, Permission = ThirdPermission },
});
var resource =
ContextualPermissionResource.WithAllPermissions([FirstPermission, SecondPermission], FirstContext);
var sut = new ContextualPermissionAuthorizer();
// act
var isDenied = sut.IsDenied(user, resource);
// assert
Assert.IsTrue(isDenied);
}
[Test]
public void IsDenied_True_HasGranularPermissionsButToSpecific()
{
// arrange
var user = BuildUser(new HashSet<IGranularPermission>
{
new DocumentGranularPermission { Permission = FirstPermission, Key = Guid.NewGuid() },
});
var resource =
ContextualPermissionResource.WithContextWidePermission(FirstPermission, DocumentGranularPermission.ContextType);
var sut = new ContextualPermissionAuthorizer();
// act
var isDenied = sut.IsDenied(user, resource);
// assert
Assert.IsTrue(isDenied);
}
[Test]
public void IsDenied_False_HasGranularPermission()
{
// arrange
var user = BuildUser(new HashSet<IGranularPermission>
{
new UnknownTypeGranularPermission { Context = FirstContext, Permission = FirstPermission },
});
var resource = ContextualPermissionResource.WithPermission(FirstPermission, FirstContext);
var sut = new ContextualPermissionAuthorizer();
// act
var isDenied = sut.IsDenied(user, resource);
// assert
Assert.IsFalse(isDenied);
}
[Test]
public void IsDenied_False_HasAnyGranularPermissions()
{
// arrange
var user = BuildUser(new HashSet<IGranularPermission>
{
new UnknownTypeGranularPermission { Context = FirstContext, Permission = SecondPermission },
});
var resource =
ContextualPermissionResource.WithAnyPermissions([FirstPermission, SecondPermission], FirstContext);
var sut = new ContextualPermissionAuthorizer();
// act
var isDenied = sut.IsDenied(user, resource);
// assert
Assert.IsFalse(isDenied);
}
[Test]
public void IsDenied_False_HasMoreThanGranularPermissions()
{
// arrange
var user = BuildUser(new HashSet<IGranularPermission>
{
new UnknownTypeGranularPermission { Context = FirstContext, Permission = FirstPermission },
new UnknownTypeGranularPermission { Context = FirstContext, Permission = SecondPermission },
});
var resource =
ContextualPermissionResource.WithAllPermissions([FirstPermission, SecondPermission], FirstContext);
var sut = new ContextualPermissionAuthorizer();
// act
var isDenied = sut.IsDenied(user, resource);
// assert
Assert.IsFalse(isDenied);
}
[Test]
public void IsDenied_False_HasSpecificGranularPermissionForAnyMatch()
{
// arrange
var user = BuildUser(new HashSet<IGranularPermission>
{
new DocumentGranularPermission { Permission = FirstPermission, Key = Guid.NewGuid() },
});
var resource =
ContextualPermissionResource.WithPermission(FirstPermission, DocumentGranularPermission.ContextType);
var sut = new ContextualPermissionAuthorizer();
// act
var isDenied = sut.IsDenied(user, resource);
// assert
Assert.IsFalse(isDenied);
}
private IUser BuildUser(ISet<IGranularPermission> granularPermissions)
{
var userGroups = new List<IReadOnlyUserGroup>();
var groupMock = new Mock<IReadOnlyUserGroup>();
groupMock
.SetupGet(group => group.GranularPermissions)
.Returns(granularPermissions);
userGroups.Add(groupMock.Object);
var userMock = new Mock<IUser>();
userMock
.SetupGet(u => u.Groups)
.Returns(userGroups);
return userMock.Object;
}
}