Implemented culture based authorization for content (#15580)
* Implemented culture based authorization for content * Implemented culture auth for create/update of documents * Applied culture authorization to dictionary create/update * Added an integration test to test an assumption about the ContentTypeEditingService.CreateAsync method * Fix processing when result is already false; * Apply suggestions from code review Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Refactor method to async + clarify and consilidate comments regarding dictionary locks --------- Co-authored-by: Sven Geusens <sge@umbraco.dk> Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
@@ -17,15 +21,18 @@ public class CreateDictionaryController : DictionaryControllerBase
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IDictionaryPresentationFactory _dictionaryPresentationFactory;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public CreateDictionaryController(
|
||||
IDictionaryItemService dictionaryItemService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IDictionaryPresentationFactory dictionaryPresentationFactory)
|
||||
IDictionaryPresentationFactory dictionaryPresentationFactory,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_dictionaryPresentationFactory = dictionaryPresentationFactory;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -38,6 +45,16 @@ public class CreateDictionaryController : DictionaryControllerBase
|
||||
{
|
||||
IDictionaryItem created = await _dictionaryPresentationFactory.MapCreateModelToDictionaryItemAsync(createDictionaryItemRequestModel);
|
||||
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
new DictionaryPermissionResource(createDictionaryItemRequestModel.Translations.Select(t => t.IsoCode)),
|
||||
AuthorizationPolicies.DictionaryPermissionByResource);
|
||||
|
||||
if (authorizationResult.Succeeded is false)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
Attempt<IDictionaryItem, DictionaryItemOperationStatus> result =
|
||||
await _dictionaryItemService.CreateAsync(created, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
|
||||
|
||||
@@ -17,15 +21,18 @@ public class UpdateDictionaryController : DictionaryControllerBase
|
||||
private readonly IDictionaryItemService _dictionaryItemService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IDictionaryPresentationFactory _dictionaryPresentationFactory;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public UpdateDictionaryController(
|
||||
IDictionaryItemService dictionaryItemService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IDictionaryPresentationFactory dictionaryPresentationFactory)
|
||||
IDictionaryPresentationFactory dictionaryPresentationFactory,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_dictionaryItemService = dictionaryItemService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_dictionaryPresentationFactory = dictionaryPresentationFactory;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpPut($"{{{nameof(id)}:guid}}")]
|
||||
@@ -41,6 +48,16 @@ public class UpdateDictionaryController : DictionaryControllerBase
|
||||
return DictionaryNotFound();
|
||||
}
|
||||
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
new DictionaryPermissionResource(updateDictionaryItemRequestModel.Translations.Select(t => t.IsoCode)),
|
||||
AuthorizationPolicies.DictionaryPermissionByResource);
|
||||
|
||||
if (authorizationResult.Succeeded is false)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IDictionaryItem updated = await _dictionaryPresentationFactory.MapUpdateModelToDictionaryItemAsync(current, updateDictionaryItemRequestModel);
|
||||
|
||||
Attempt<IDictionaryItem, DictionaryItemOperationStatus> result =
|
||||
|
||||
@@ -45,7 +45,12 @@ public class CreateDocumentController : DocumentControllerBase
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.ParentId),
|
||||
ContentPermissionResource.WithKeys(
|
||||
ActionNew.ActionLetter,
|
||||
requestModel.ParentId,
|
||||
requestModel.Variants
|
||||
.Where(v => v.Culture is not null)
|
||||
.Select(v => v.Culture!)),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.UserGroup;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
@@ -40,7 +41,7 @@ public class PublishDocumentController : DocumentControllerBase
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id),
|
||||
ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.Cultures),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
|
||||
@@ -40,7 +40,7 @@ public class PublishDocumentWithDescendantsController : DocumentControllerBase
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.Branch(ActionPublish.ActionLetter, id),
|
||||
ContentPermissionResource.Branch(ActionPublish.ActionLetter, id, requestModel.Cultures),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
|
||||
@@ -41,7 +41,10 @@ public class UnpublishDocumentController : DocumentControllerBase
|
||||
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionUnpublish.ActionLetter, id),
|
||||
ContentPermissionResource.WithKeys(
|
||||
ActionUnpublish.ActionLetter,
|
||||
id,
|
||||
requestModel.Culture is not null ? requestModel.Culture.Yield() : Enumerable.Empty<string>()),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
|
||||
@@ -45,7 +45,12 @@ public class UpdateDocumentController : DocumentControllerBase
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id),
|
||||
ContentPermissionResource.WithKeys(
|
||||
ActionUpdate.ActionLetter,
|
||||
id,
|
||||
requestModel.Variants
|
||||
.Where(v => v.Culture is not null)
|
||||
.Select(v => v.Culture!)),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
|
||||
@@ -4,6 +4,7 @@ using OpenIddict.Validation.AspNetCore;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.DenyLocalLogin;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Feature;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Media;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.User;
|
||||
@@ -28,6 +29,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, MediaPermissionHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, UserGroupPermissionHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, DictionaryPermissionHandler>();
|
||||
|
||||
builder.Services.AddSingleton<IAuthorizationHelper, AuthorizationHelper>();
|
||||
builder.Services.AddSingleton<IContentPermissionAuthorizer, ContentPermissionAuthorizer>();
|
||||
@@ -35,6 +37,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
builder.Services.AddSingleton<IMediaPermissionAuthorizer, MediaPermissionAuthorizer>();
|
||||
builder.Services.AddSingleton<IUserGroupPermissionAuthorizer, UserGroupPermissionAuthorizer>();
|
||||
builder.Services.AddSingleton<IUserPermissionAuthorizer, UserPermissionAuthorizer>();
|
||||
builder.Services.AddSingleton<IDictionaryPermissionAuthorizer, DictionaryPermissionAuthorizer>();
|
||||
|
||||
builder.Services.AddAuthorization(CreatePolicies);
|
||||
return builder;
|
||||
@@ -131,5 +134,11 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
|
||||
policy.Requirements.Add(new UserGroupPermissionRequirement());
|
||||
});
|
||||
|
||||
options.AddPolicy($"New{AuthorizationPolicies.DictionaryPermissionByResource}", policy =>
|
||||
{
|
||||
policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
|
||||
policy.Requirements.Add(new DictionaryPermissionRequirement());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +62,11 @@ internal sealed class ContentPermissionAuthorizer : IContentPermissionAuthorizer
|
||||
|
||||
return result == ContentAuthorizationStatus.Success;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAuthorizedForCultures(IPrincipal currentUser, ISet<string> culturesToCheck)
|
||||
{
|
||||
IUser user = _authorizationHelper.GetUmbracoUser(currentUser);
|
||||
ContentAuthorizationStatus result = await _contentPermissionService.AuthorizeCultureAccessAsync(user, culturesToCheck);
|
||||
return result is ContentAuthorizationStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,28 +22,36 @@ public class ContentPermissionHandler : MustSatisfyRequirementAuthorizationHandl
|
||||
ContentPermissionRequirement requirement,
|
||||
ContentPermissionResource resource)
|
||||
{
|
||||
var result = true;
|
||||
|
||||
if (resource.CheckRoot)
|
||||
if (resource.CheckRoot
|
||||
&& await _contentPermissionAuthorizer.IsAuthorizedAtRootLevelAsync(context.User, resource.PermissionsToCheck) is false)
|
||||
{
|
||||
result &= await _contentPermissionAuthorizer.IsAuthorizedAtRootLevelAsync(context.User, resource.PermissionsToCheck);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resource.CheckRecycleBin)
|
||||
if (resource.CheckRecycleBin
|
||||
&& await _contentPermissionAuthorizer.IsAuthorizedAtRecycleBinLevelAsync(context.User, resource.PermissionsToCheck) is false)
|
||||
{
|
||||
result &= await _contentPermissionAuthorizer.IsAuthorizedAtRecycleBinLevelAsync(context.User, resource.PermissionsToCheck);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resource.ParentKeyForBranch is not null)
|
||||
if (resource.ParentKeyForBranch is not null
|
||||
&& await _contentPermissionAuthorizer.IsAuthorizedWithDescendantsAsync(context.User, resource.ParentKeyForBranch.Value, resource.PermissionsToCheck) is false)
|
||||
{
|
||||
result &= await _contentPermissionAuthorizer.IsAuthorizedWithDescendantsAsync(context.User, resource.ParentKeyForBranch.Value, resource.PermissionsToCheck);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resource.ContentKeys.Any())
|
||||
if (resource.ContentKeys.Any()
|
||||
&& await _contentPermissionAuthorizer.IsAuthorizedAsync(context.User, resource.ContentKeys, resource.PermissionsToCheck) is false)
|
||||
{
|
||||
result &= await _contentPermissionAuthorizer.IsAuthorizedAsync(context.User, resource.ContentKeys, resource.PermissionsToCheck);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resource.CulturesToCheck is not null
|
||||
&& await _contentPermissionAuthorizer.IsAuthorizedForCultures(context.User, resource.CulturesToCheck) is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return result;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,18 @@ public class ContentPermissionResource : IPermissionResource
|
||||
? Root(permissionToCheck)
|
||||
: WithKeys(permissionToCheck, contentKey.Value.Yield());
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permission and content key or root.
|
||||
/// </summary>
|
||||
/// <param name="permissionToCheck">The permission to check for.</param>
|
||||
/// <param name="contentKey">The key of the content or null if root.</param>
|
||||
/// <param name="cultures">The cultures to validate</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource WithKeys(char permissionToCheck, Guid? contentKey, IEnumerable<string> cultures) =>
|
||||
contentKey is null
|
||||
? Root(permissionToCheck, cultures)
|
||||
: WithKeys(permissionToCheck, contentKey.Value.Yield(), cultures);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permission and content keys.
|
||||
/// </summary>
|
||||
@@ -29,7 +41,7 @@ public class ContentPermissionResource : IPermissionResource
|
||||
var hasRoot = contentKeys.Any(x => x is null);
|
||||
IEnumerable<Guid> keys = contentKeys.Where(x => x.HasValue).Select(x => x!.Value);
|
||||
|
||||
return new ContentPermissionResource(keys, new HashSet<char> { permissionToCheck }, hasRoot, false, null);
|
||||
return new ContentPermissionResource(keys, new HashSet<char> { permissionToCheck }, hasRoot, false, null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,6 +52,15 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource WithKeys(char permissionToCheck, Guid contentKey) => WithKeys(permissionToCheck, contentKey.Yield());
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permission and content key.
|
||||
/// </summary>
|
||||
/// <param name="permissionToCheck">The permission to check for.</param>
|
||||
/// <param name="contentKey">The key of the content.</param>
|
||||
/// <param name="cultures">The required culture access</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource WithKeys(char permissionToCheck, Guid contentKey,IEnumerable<string> cultures) => WithKeys(permissionToCheck, contentKey.Yield(),cultures);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permission and content keys.
|
||||
/// </summary>
|
||||
@@ -47,7 +68,23 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="contentKeys">The keys of the contents.</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource WithKeys(char permissionToCheck, IEnumerable<Guid> contentKeys) =>
|
||||
new ContentPermissionResource(contentKeys, new HashSet<char> { permissionToCheck }, false, false, null);
|
||||
new ContentPermissionResource(contentKeys, new HashSet<char> { permissionToCheck }, false, false, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permission and content keys.
|
||||
/// </summary>
|
||||
/// <param name="permissionToCheck">The permission to check for.</param>
|
||||
/// <param name="contentKeys">The keys of the contents.</param>
|
||||
/// <param name="cultures">The required culture access</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource WithKeys(char permissionToCheck, IEnumerable<Guid> contentKeys, IEnumerable<string> cultures) =>
|
||||
new ContentPermissionResource(
|
||||
contentKeys,
|
||||
new HashSet<char> { permissionToCheck },
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
new HashSet<string>(cultures.Distinct()));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permissions and content keys.
|
||||
@@ -56,7 +93,7 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="contentKeys">The keys of the contents.</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource WithKeys(ISet<char> permissionsToCheck, IEnumerable<Guid> contentKeys) =>
|
||||
new ContentPermissionResource(contentKeys, permissionsToCheck, false, false, null);
|
||||
new ContentPermissionResource(contentKeys, permissionsToCheck, false, false, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permission and the root.
|
||||
@@ -64,7 +101,16 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="permissionToCheck">The permission to check for.</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource Root(char permissionToCheck) =>
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), new HashSet<char> { permissionToCheck }, true, false, null);
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), new HashSet<char> { permissionToCheck }, true, false, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permission and the root.
|
||||
/// </summary>
|
||||
/// <param name="permissionToCheck">The permission to check for.</param>
|
||||
/// <param name="cultures">The cultures to validate</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource Root(char permissionToCheck, IEnumerable<string> cultures) =>
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), new HashSet<char> { permissionToCheck }, true, false, null, new HashSet<string>(cultures));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permissions and the root.
|
||||
@@ -72,7 +118,18 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="permissionsToCheck">The permissions to check for.</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource Root(ISet<char> permissionsToCheck) =>
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), permissionsToCheck, true, false, null);
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), permissionsToCheck, true, false, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permissions and the root.
|
||||
/// </summary>
|
||||
/// <param name="permissionsToCheck">The permissions to check for.</param>
|
||||
/// <param name="cultures">The cultures to validate</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource Root(ISet<char> permissionsToCheck, IEnumerable<string> cultures) =>
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), permissionsToCheck, true, false, null, new HashSet<string>(cultures));
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permissions and the recycle bin.
|
||||
@@ -80,7 +137,7 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="permissionsToCheck">The permissions to check for.</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource RecycleBin(ISet<char> permissionsToCheck) =>
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), permissionsToCheck, false, true, null);
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), permissionsToCheck, false, true, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permission and the recycle bin.
|
||||
@@ -88,7 +145,7 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="permissionToCheck">The permission to check for.</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource RecycleBin(char permissionToCheck) =>
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), new HashSet<char> { permissionToCheck }, false, true, null);
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), new HashSet<char> { permissionToCheck }, false, true, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permissions and the branch from the specified parent key.
|
||||
@@ -97,7 +154,7 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="parentKeyForBranch">The parent key of the branch.</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource Branch(ISet<char> permissionsToCheck, Guid parentKeyForBranch) =>
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), permissionsToCheck, false, true, parentKeyForBranch);
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), permissionsToCheck, false, true, parentKeyForBranch, null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permission and the branch from the specified parent key.
|
||||
@@ -106,15 +163,37 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="parentKeyForBranch">The parent key of the branch.</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource Branch(char permissionToCheck, Guid parentKeyForBranch) =>
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), new HashSet<char> { permissionToCheck }, false, true, parentKeyForBranch);
|
||||
new ContentPermissionResource(Enumerable.Empty<Guid>(), new HashSet<char> { permissionToCheck }, false, true, parentKeyForBranch, null);
|
||||
|
||||
private ContentPermissionResource(IEnumerable<Guid> contentKeys, ISet<char> permissionsToCheck, bool checkRoot, bool checkRecycleBin, Guid? parentKeyForBranch)
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ContentPermissionResource" /> with the specified permission and the branch from the specified parent key.
|
||||
/// </summary>
|
||||
/// <param name="permissionToCheck">The permission to check for.</param>
|
||||
/// <param name="parentKeyForBranch">The parent key of the branch.</param>
|
||||
/// <param name="culturesToCheck">The required cultures</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource Branch(char permissionToCheck, Guid parentKeyForBranch, IEnumerable<string> culturesToCheck) =>
|
||||
new ContentPermissionResource(
|
||||
Enumerable.Empty<Guid>(),
|
||||
new HashSet<char> { permissionToCheck },
|
||||
false,
|
||||
true,
|
||||
parentKeyForBranch,
|
||||
new HashSet<string>(culturesToCheck.Distinct()));
|
||||
|
||||
private ContentPermissionResource(
|
||||
IEnumerable<Guid> contentKeys,
|
||||
ISet<char> permissionsToCheck,
|
||||
bool checkRoot, bool checkRecycleBin,
|
||||
Guid? parentKeyForBranch,
|
||||
ISet<string>? culturesToCheck)
|
||||
{
|
||||
ContentKeys = contentKeys;
|
||||
PermissionsToCheck = permissionsToCheck;
|
||||
CheckRoot = checkRoot;
|
||||
CheckRecycleBin = checkRecycleBin;
|
||||
ParentKeyForBranch = parentKeyForBranch;
|
||||
CulturesToCheck = culturesToCheck;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -144,4 +223,9 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// Gets the parent key of a branch.
|
||||
/// </summary>
|
||||
public Guid? ParentKeyForBranch { get; }
|
||||
|
||||
/// <summary>
|
||||
/// All the cultures need to be accessible when evaluating
|
||||
/// </summary>
|
||||
public ISet<string>? CulturesToCheck { get; }
|
||||
}
|
||||
|
||||
@@ -79,4 +79,6 @@ public interface IContentPermissionAuthorizer
|
||||
/// <param name="permissionsToCheck">The collection of permissions to authorize.</param>
|
||||
/// <returns>Returns <c>true</c> if authorization is successful, otherwise <c>false</c>.</returns>
|
||||
Task<bool> IsAuthorizedAtRecycleBinLevelAsync(IPrincipal currentUser, ISet<char> permissionsToCheck);
|
||||
|
||||
Task<bool> IsAuthorizedForCultures(IPrincipal currentUser, ISet<string> culturesToCheck);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Security.Principal;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.AuthorizationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
|
||||
|
||||
public class DictionaryPermissionAuthorizer : IDictionaryPermissionAuthorizer
|
||||
{
|
||||
private readonly IAuthorizationHelper _authorizationHelper;
|
||||
private readonly IDictionaryPermissionService _dictionaryPermissionService;
|
||||
|
||||
public DictionaryPermissionAuthorizer(IAuthorizationHelper authorizationHelper, IDictionaryPermissionService dictionaryPermissionService)
|
||||
{
|
||||
_authorizationHelper = authorizationHelper;
|
||||
_dictionaryPermissionService = dictionaryPermissionService;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAuthorizedForCultures(IPrincipal currentUser, ISet<string> culturesToCheck)
|
||||
{
|
||||
IUser user = _authorizationHelper.GetUmbracoUser(currentUser);
|
||||
DictionaryAuthorizationStatus result = await _dictionaryPermissionService.AuthorizeCultureAccessAsync(user, culturesToCheck);
|
||||
return result is DictionaryAuthorizationStatus.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
|
||||
|
||||
/// <summary>
|
||||
/// Authorizes that the current user has the correct permission access to the dictionary item(s) specified in the request.
|
||||
/// </summary>
|
||||
public class DictionaryPermissionHandler : MustSatisfyRequirementAuthorizationHandler<DictionaryPermissionRequirement, DictionaryPermissionResource>
|
||||
{
|
||||
private readonly IDictionaryPermissionAuthorizer _dictionaryPermissionAuthorizer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DictionaryPermissionHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="dictionaryPermissionAuthorizer">Authorizer for content access.</param>
|
||||
public DictionaryPermissionHandler(IDictionaryPermissionAuthorizer dictionaryPermissionAuthorizer)
|
||||
=> _dictionaryPermissionAuthorizer = dictionaryPermissionAuthorizer;
|
||||
|
||||
protected override async Task<bool> IsAuthorized(AuthorizationHandlerContext context, DictionaryPermissionRequirement requirement,
|
||||
DictionaryPermissionResource resource)
|
||||
{
|
||||
if (resource.CulturesToCheck.Any()
|
||||
&& await _dictionaryPermissionAuthorizer.IsAuthorizedForCultures(context.User, resource.CulturesToCheck) is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
|
||||
|
||||
/// <summary>
|
||||
/// Authorization requirement for the <see cref="DictionaryPermissionHandler" />.
|
||||
/// </summary>
|
||||
public class DictionaryPermissionRequirement : IAuthorizationRequirement
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
|
||||
|
||||
public class DictionaryPermissionResource : IPermissionResource
|
||||
{
|
||||
public DictionaryPermissionResource(IEnumerable<string> cultures)
|
||||
{
|
||||
CulturesToCheck = new HashSet<string>(cultures);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All the cultures need to be accessible when evaluating
|
||||
/// </summary>
|
||||
public ISet<string> CulturesToCheck { get; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
|
||||
|
||||
public interface IDictionaryPermissionAuthorizer
|
||||
{
|
||||
Task<bool> IsAuthorizedForCultures(IPrincipal currentUser, ISet<string> culturesToCheck);
|
||||
}
|
||||
@@ -302,6 +302,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddUnique<IDomainService, DomainService>();
|
||||
Services.AddUnique<ITagService, TagService>();
|
||||
Services.AddUnique<IContentPermissionService, ContentPermissionService>();
|
||||
Services.AddUnique<IDictionaryPermissionService, DictionaryPermissionService>();
|
||||
Services.AddUnique<IContentService, ContentService>();
|
||||
Services.AddUnique<IContentEditingService, ContentEditingService>();
|
||||
Services.AddUnique<IContentPublishingService, ContentPublishingService>();
|
||||
|
||||
@@ -37,4 +37,6 @@ public interface ILanguageRepository : IReadWriteQueryRepository<int, ILanguage>
|
||||
/// <para>This can be optimized and bypass all deep cloning.</para>
|
||||
/// </remarks>
|
||||
int? GetDefaultId();
|
||||
|
||||
string[] GetIsoCodesByIds(ICollection<int> ids, bool throwOnNotFound = true);
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ public enum ContentAuthorizationStatus
|
||||
UnauthorizedMissingBinAccess,
|
||||
UnauthorizedMissingDescendantAccess,
|
||||
UnauthorizedMissingPathAccess,
|
||||
UnauthorizedMissingRootAccess
|
||||
UnauthorizedMissingRootAccess,
|
||||
UnauthorizedMissingCulture
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Umbraco.Cms.Core.Services.AuthorizationStatus;
|
||||
|
||||
public enum DictionaryAuthorizationStatus
|
||||
{
|
||||
Success,
|
||||
UnauthorizedMissingCulture
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services.AuthorizationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -14,17 +15,20 @@ internal sealed class ContentPermissionService : IContentPermissionService
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly AppCaches _appCaches;
|
||||
private readonly ILanguageService _languageService;
|
||||
|
||||
public ContentPermissionService(
|
||||
IContentService contentService,
|
||||
IEntityService entityService,
|
||||
IUserService userService,
|
||||
AppCaches appCaches)
|
||||
AppCaches appCaches,
|
||||
ILanguageService languageService)
|
||||
{
|
||||
_contentService = contentService;
|
||||
_entityService = entityService;
|
||||
_userService = userService;
|
||||
_appCaches = appCaches;
|
||||
_languageService = languageService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -130,6 +134,22 @@ internal sealed class ContentPermissionService : IContentPermissionService
|
||||
: ContentAuthorizationStatus.UnauthorizedMissingPathAccess;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ContentAuthorizationStatus> AuthorizeCultureAccessAsync(IUser user, ISet<string> culturesToCheck)
|
||||
{
|
||||
if (user.Groups.Any(group => group.HasAccessToAllLanguages))
|
||||
{
|
||||
return ContentAuthorizationStatus.Success;
|
||||
}
|
||||
|
||||
var allowedLanguages = user.Groups.SelectMany(g => g.AllowedLanguages).Distinct().ToArray();
|
||||
var allowedLanguageIsoCodes = await _languageService.GetIsoCodesByIdsAsync(allowedLanguages);
|
||||
|
||||
return culturesToCheck.All(culture => allowedLanguageIsoCodes.InvariantContains(culture))
|
||||
? ContentAuthorizationStatus.Success
|
||||
: ContentAuthorizationStatus.UnauthorizedMissingCulture;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check the implicit/inherited permissions of a user for given content items.
|
||||
/// </summary>
|
||||
|
||||
31
src/Umbraco.Core/Services/DictionaryPermissionService.cs
Normal file
31
src/Umbraco.Core/Services/DictionaryPermissionService.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services.AuthorizationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public class DictionaryPermissionService : IDictionaryPermissionService
|
||||
{
|
||||
private readonly ILanguageService _languageService;
|
||||
|
||||
public DictionaryPermissionService(ILanguageService languageService)
|
||||
{
|
||||
_languageService = languageService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<DictionaryAuthorizationStatus> AuthorizeCultureAccessAsync(IUser user, ISet<string> culturesToCheck)
|
||||
{
|
||||
if (user.Groups.Any(group => group.HasAccessToAllLanguages))
|
||||
{
|
||||
return DictionaryAuthorizationStatus.Success;
|
||||
}
|
||||
|
||||
var allowedLanguages = user.Groups.SelectMany(g => g.AllowedLanguages).Distinct().ToArray();
|
||||
var allowedLanguageIsoCodes = await _languageService.GetIsoCodesByIdsAsync(allowedLanguages);
|
||||
|
||||
return culturesToCheck.All(culture => allowedLanguageIsoCodes.InvariantContains(culture))
|
||||
? DictionaryAuthorizationStatus.Success
|
||||
: DictionaryAuthorizationStatus.UnauthorizedMissingCulture;
|
||||
}
|
||||
}
|
||||
@@ -80,4 +80,12 @@ public interface IContentPermissionService
|
||||
/// <param name="permissionsToCheck">The collection of permissions to authorize.</param>
|
||||
/// <returns>A task resolving into a <see cref="ContentAuthorizationStatus"/>.</returns>
|
||||
Task<ContentAuthorizationStatus> AuthorizeBinAccessAsync(IUser user, ISet<char> permissionsToCheck);
|
||||
|
||||
/// <summary>
|
||||
/// Authorize that a user has access to specific cultures
|
||||
/// </summary>
|
||||
/// <param name="user"><see cref="IUser" /> to authorize.</param>
|
||||
/// <param name="culturesToCheck">The collection of cultures to authorize.</param>
|
||||
/// <returns>A task resolving into a <see cref="ContentAuthorizationStatus"/>.</returns>
|
||||
Task<ContentAuthorizationStatus> AuthorizeCultureAccessAsync(IUser user, ISet<string> culturesToCheck);
|
||||
}
|
||||
|
||||
10
src/Umbraco.Core/Services/IDictionaryPermissionService.cs
Normal file
10
src/Umbraco.Core/Services/IDictionaryPermissionService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services.AuthorizationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface IDictionaryPermissionService
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
Task<DictionaryAuthorizationStatus> AuthorizeCultureAccessAsync(IUser user, ISet<string> culturesToCheck);
|
||||
}
|
||||
@@ -55,4 +55,12 @@ public interface ILanguageService
|
||||
/// <param name="isoCode">The ISO code of the <see cref="ILanguage" /> to delete</param>
|
||||
/// <param name="userKey">Key of the user deleting the language</param>
|
||||
Task<Attempt<ILanguage?, LanguageOperationStatus>> DeleteAsync(string isoCode, Guid userKey);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the isoCodes of configured languages by their Ids
|
||||
/// </summary>
|
||||
/// <param name="ids">The ids of the configured <see cref="ILanguage" />s</param>
|
||||
/// <returns>The ISO codes of the <see cref="ILanguage" />s</returns>
|
||||
Task<string[]> GetIsoCodesByIdsAsync(ICollection<int> ids);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,13 @@ internal sealed class LanguageService : RepositoryService, ILanguageService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string[]> GetIsoCodesByIdsAsync(ICollection<int> ids)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete:true);
|
||||
|
||||
return await Task.FromResult(_languageRepository.GetIsoCodesByIds(ids, throwOnNotFound: true));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ILanguage>> GetMultipleAsync(IEnumerable<string> isoCodes) => (await GetAllAsync()).Where(x => isoCodes.Contains(x.IsoCode));
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
/// </summary>
|
||||
internal class LanguageRepository : EntityRepositoryBase<int, ILanguage>, ILanguageRepository
|
||||
{
|
||||
// We need to lock this dictionary every time we do an operation on it as the languageRepository is registered as a unique implementation
|
||||
// It is used to quickly get isoCodes by Id, or the reverse by avoiding (deep)cloning dtos
|
||||
// It is rebuild on PerformGetAll
|
||||
private readonly Dictionary<string, int> _codeIdMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<int, string> _idCodeMap = new();
|
||||
|
||||
@@ -37,8 +40,6 @@ internal class LanguageRepository : EntityRepositoryBase<int, ILanguage>, ILangu
|
||||
return id.HasValue ? Get(id.Value) : null;
|
||||
}
|
||||
|
||||
// fast way of getting an id for an isoCode - avoiding cloning
|
||||
// _codeIdMap is rebuilt whenever PerformGetAll runs
|
||||
public int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true)
|
||||
{
|
||||
if (isoCode == null)
|
||||
@@ -64,8 +65,6 @@ internal class LanguageRepository : EntityRepositoryBase<int, ILanguage>, ILangu
|
||||
return null;
|
||||
}
|
||||
|
||||
// fast way of getting an isoCode for an id - avoiding cloning
|
||||
// _idCodeMap is rebuilt whenever PerformGetAll runs
|
||||
public string? GetIsoCodeById(int? id, bool throwOnNotFound = true)
|
||||
{
|
||||
if (id == null)
|
||||
@@ -75,7 +74,6 @@ internal class LanguageRepository : EntityRepositoryBase<int, ILanguage>, ILangu
|
||||
|
||||
EnsureCacheIsPopulated();
|
||||
|
||||
// yes, we want to lock _codeIdMap
|
||||
lock (_codeIdMap)
|
||||
{
|
||||
if (_idCodeMap.TryGetValue(id.Value, out var isoCode))
|
||||
@@ -92,6 +90,38 @@ internal class LanguageRepository : EntityRepositoryBase<int, ILanguage>, ILangu
|
||||
return null;
|
||||
}
|
||||
|
||||
// multi implementation of GetIsoCodeById
|
||||
public string[] GetIsoCodesByIds(ICollection<int> ids, bool throwOnNotFound = true)
|
||||
{
|
||||
var isoCodes = new string[ids.Count];
|
||||
|
||||
if (ids.Any() == false)
|
||||
{
|
||||
return isoCodes;
|
||||
}
|
||||
|
||||
EnsureCacheIsPopulated();
|
||||
|
||||
|
||||
lock (_codeIdMap)
|
||||
{
|
||||
for (var i = 0; i < ids.Count; i++)
|
||||
{
|
||||
var id = ids.ElementAt(i);
|
||||
if (_idCodeMap.TryGetValue(id, out var isoCode))
|
||||
{
|
||||
isoCodes[i] = isoCode;
|
||||
}
|
||||
else if (throwOnNotFound)
|
||||
{
|
||||
throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isoCodes;
|
||||
}
|
||||
|
||||
public string GetDefaultIsoCode() => GetDefault().IsoCode;
|
||||
|
||||
public int? GetDefaultId() => GetDefault().Id;
|
||||
@@ -101,7 +131,6 @@ internal class LanguageRepository : EntityRepositoryBase<int, ILanguage>, ILangu
|
||||
|
||||
protected ILanguage ConvertFromDto(LanguageDto dto)
|
||||
{
|
||||
// yes, we want to lock _codeIdMap
|
||||
lock (_codeIdMap)
|
||||
{
|
||||
string? fallbackIsoCode = null;
|
||||
|
||||
@@ -72,6 +72,9 @@ public static class AuthorizationPolicies
|
||||
public const string TreeAccessAnySchemaTypes = nameof(TreeAccessAnySchemaTypes);
|
||||
public const string TreeAccessDictionaryOrTemplates = nameof(TreeAccessDictionaryOrTemplates);
|
||||
|
||||
// other
|
||||
public const string DictionaryPermissionByResource = nameof(DictionaryPermissionByResource);
|
||||
|
||||
/// <summary>
|
||||
/// Defines access based on if the user has access to any tree's exposing any types of content (documents, media,
|
||||
/// members)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(
|
||||
Database = UmbracoTestOptions.Database.NewSchemaPerTest,
|
||||
PublishedRepositoryEvents = true,
|
||||
WithApplication = true)]
|
||||
public class ContentEditingServiceTests : UmbracoIntegrationTestWithContent
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup() => ContentRepositoryBase.ThrowOnWarning = true;
|
||||
|
||||
[TearDown]
|
||||
public void Teardown() => ContentRepositoryBase.ThrowOnWarning = false;
|
||||
|
||||
private IContentEditingService ContentTypeEditingService => GetRequiredService<IContentEditingService>();
|
||||
private ILanguageService LanguageService => GetRequiredService<ILanguageService>();
|
||||
|
||||
[Test]
|
||||
public async Task Only_Supplied_Cultures_Are_Updated()
|
||||
{
|
||||
var variantTestData = await SetupVariantTest();
|
||||
var documentKey = Guid.NewGuid();
|
||||
var propertyAlias = "title";
|
||||
var originalPropertyValue = "original";
|
||||
var updatedPropertyValue = "updated";
|
||||
|
||||
var createModel = new ContentCreateModel
|
||||
{
|
||||
Key = documentKey,
|
||||
ContentTypeKey = variantTestData.contentType.Key,
|
||||
Variants = new[]
|
||||
{
|
||||
new VariantModel
|
||||
{
|
||||
Name = variantTestData.LangEn.CultureName,
|
||||
Culture = variantTestData.LangEn.IsoCode,
|
||||
Properties = new[]
|
||||
{
|
||||
new PropertyValueModel
|
||||
{
|
||||
Alias = propertyAlias, Value = originalPropertyValue
|
||||
}
|
||||
}
|
||||
},
|
||||
new VariantModel
|
||||
{
|
||||
Name = variantTestData.LangDa.CultureName,
|
||||
Culture = variantTestData.LangDa.IsoCode,
|
||||
Properties = new[]
|
||||
{
|
||||
new PropertyValueModel
|
||||
{
|
||||
Alias = propertyAlias, Value = originalPropertyValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
|
||||
|
||||
var content = ContentService.GetById(documentKey)!;
|
||||
|
||||
var updateModel = new ContentUpdateModel
|
||||
{
|
||||
Variants = new[]
|
||||
{
|
||||
new VariantModel
|
||||
{
|
||||
Name = updatedPropertyValue,
|
||||
Culture = variantTestData.LangEn.IsoCode,
|
||||
Properties = new[] { new PropertyValueModel { Alias = propertyAlias, Value = updatedPropertyValue } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await ContentTypeEditingService.UpdateAsync(content, updateModel, Constants.Security.SuperUserKey);
|
||||
|
||||
var updatedContent = ContentService.GetById(documentKey)!;
|
||||
|
||||
Assert.AreEqual(originalPropertyValue, updatedContent.GetValue(propertyAlias,variantTestData.LangDa.IsoCode));
|
||||
Assert.AreEqual(updatedPropertyValue, updatedContent.GetValue(propertyAlias,variantTestData.LangEn.IsoCode));
|
||||
|
||||
Assert.AreEqual(variantTestData.LangDa.CultureName, updatedContent.GetCultureName(variantTestData.LangDa.IsoCode));
|
||||
Assert.AreEqual(updatedPropertyValue, updatedContent.GetCultureName(variantTestData.LangEn.IsoCode));
|
||||
}
|
||||
|
||||
private async Task<(ILanguage LangEn, ILanguage LangDa, IContentType contentType)> SetupVariantTest()
|
||||
{
|
||||
var langEn = (await LanguageService.GetAsync("en-US"))!;
|
||||
var langDa = new LanguageBuilder()
|
||||
.WithCultureInfo("da-DK")
|
||||
.Build();
|
||||
await LanguageService.CreateAsync(langDa, Constants.Security.SuperUserKey);
|
||||
|
||||
var template = TemplateBuilder.CreateTextPageTemplate();
|
||||
FileService.SaveTemplate(template);
|
||||
|
||||
var contentType = new ContentTypeBuilder()
|
||||
.WithAlias("variantContent")
|
||||
.WithName("Variant Content")
|
||||
.WithContentVariation(ContentVariation.Culture)
|
||||
.AddPropertyGroup()
|
||||
.WithAlias("content")
|
||||
.WithName("Content")
|
||||
.WithSupportsPublishing(true)
|
||||
.AddPropertyType()
|
||||
.WithAlias("title")
|
||||
.WithName("Title")
|
||||
.WithVariations(ContentVariation.Culture)
|
||||
.WithMandatory(true)
|
||||
.Done()
|
||||
.Done()
|
||||
.Build();
|
||||
|
||||
contentType.AllowedAsRoot = true;
|
||||
ContentTypeService.Save(contentType);
|
||||
|
||||
return (langEn, langDa, contentType);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user