Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ae5cdc147 | ||
|
|
bf4da0d4f7 | ||
|
|
b6201764db | ||
|
|
21f1641fb4 | ||
|
|
31b4e6b461 | ||
|
|
51af775761 | ||
|
|
74be0762ba | ||
|
|
247ced31bd | ||
|
|
05ece430d8 |
+8
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+70
@@ -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" },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -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;
|
||||
}
|
||||
}
|
||||
+7
@@ -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);
|
||||
}
|
||||
+210
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user