migrates the custom content persmission helper and media permissions helper checks to authz policies and cleans up that code/class/namespaces
This commit is contained in:
@@ -638,9 +638,9 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <returns></returns>
|
||||
[FileUploadCleanupFilter]
|
||||
[ContentSaveValidation]
|
||||
public ContentItemDisplay PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem)
|
||||
public async Task<ContentItemDisplay> PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem)
|
||||
{
|
||||
var contentItemDisplay = PostSaveInternal(contentItem,
|
||||
var contentItemDisplay = await PostSaveInternal(contentItem,
|
||||
content =>
|
||||
{
|
||||
EnsureUniqueName(content.Name, content, "Name");
|
||||
@@ -666,9 +666,9 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
[FileUploadCleanupFilter]
|
||||
[ContentSaveValidation]
|
||||
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
|
||||
public ContentItemDisplay PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem)
|
||||
public async Task<ContentItemDisplay> PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem)
|
||||
{
|
||||
var contentItemDisplay = PostSaveInternal(
|
||||
var contentItemDisplay = await PostSaveInternal(
|
||||
contentItem,
|
||||
content => _contentService.Save(contentItem.PersistedContent, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id),
|
||||
MapToDisplay);
|
||||
@@ -676,7 +676,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
return contentItemDisplay;
|
||||
}
|
||||
|
||||
private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func<IContent, OperationResult> saveMethod, Func<IContent, ContentItemDisplay> mapToDisplay)
|
||||
private async Task<ContentItemDisplay> PostSaveInternal(ContentItemSave contentItem, Func<IContent, OperationResult> saveMethod, Func<IContent, ContentItemDisplay> mapToDisplay)
|
||||
{
|
||||
//Recent versions of IE/Edge may send in the full client side file path instead of just the file name.
|
||||
//To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all
|
||||
@@ -811,7 +811,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
case ContentSaveAction.PublishWithDescendants:
|
||||
case ContentSaveAction.PublishWithDescendantsNew:
|
||||
{
|
||||
if (!ValidatePublishBranchPermissions(contentItem, out var noAccess))
|
||||
if (!await ValidatePublishBranchPermissionsAsync(contentItem))
|
||||
{
|
||||
globalNotifications.AddErrorNotification(
|
||||
_localizedTextService.Localize("publish"),
|
||||
@@ -827,7 +827,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
case ContentSaveAction.PublishWithDescendantsForce:
|
||||
case ContentSaveAction.PublishWithDescendantsForceNew:
|
||||
{
|
||||
if (!ValidatePublishBranchPermissions(contentItem, out var noAccess))
|
||||
if (!await ValidatePublishBranchPermissionsAsync(contentItem))
|
||||
{
|
||||
globalNotifications.AddErrorNotification(
|
||||
_localizedTextService.Localize("publish"),
|
||||
@@ -1198,33 +1198,12 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <param name="contentItem"></param>
|
||||
/// <returns></returns>
|
||||
private bool ValidatePublishBranchPermissions(ContentItemSave contentItem, out IReadOnlyList<IUmbracoEntity> noAccess)
|
||||
private async Task<bool> ValidatePublishBranchPermissionsAsync(ContentItemSave contentItem)
|
||||
{
|
||||
var denied = new List<IUmbracoEntity>();
|
||||
var page = 0;
|
||||
const int pageSize = 500;
|
||||
var total = long.MaxValue;
|
||||
while (page * pageSize < total)
|
||||
{
|
||||
var descendants = _entityService.GetPagedDescendants(contentItem.Id, UmbracoObjectTypes.Document, page++, pageSize, out total,
|
||||
//order by shallowest to deepest, this allows us to check permissions from top to bottom so we can exit
|
||||
//early if a permission higher up fails
|
||||
ordering: Ordering.By("path", Direction.Ascending));
|
||||
|
||||
foreach (var c in descendants)
|
||||
{
|
||||
//if this item's path has already been denied or if the user doesn't have access to it, add to the deny list
|
||||
if (denied.Any(x => c.Path.StartsWith($"{x.Path},"))
|
||||
|| (ContentPermissionsHelper.CheckPermissions(c,
|
||||
_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, _userService, _entityService,
|
||||
ActionPublish.ActionLetter) == ContentPermissionsHelper.ContentAccess.Denied))
|
||||
{
|
||||
denied.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
noAccess = denied;
|
||||
return denied.Count == 0;
|
||||
// Authorize...
|
||||
var requirement = new ContentPermissionsPublishBranchRequirement(ActionPublish.ActionLetter);
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, contentItem.PersistedContent, requirement);
|
||||
return authorizationResult.Succeeded;
|
||||
}
|
||||
|
||||
private IEnumerable<PublishResult> PublishBranchInternal(ContentItemSave contentItem, bool force, string cultureForInvariantErrors,
|
||||
|
||||
@@ -42,6 +42,7 @@ using Umbraco.Web.Models.ContentEditing;
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Umbraco.Web.Common.Authorization;
|
||||
using Umbraco.Web.BackOffice.Authorization;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Controllers
|
||||
{
|
||||
@@ -67,6 +68,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
private readonly IRelationService _relationService;
|
||||
private readonly IImageUrlGenerator _imageUrlGenerator;
|
||||
private readonly IJsonSerializer _serializer;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly ILogger<MediaController> _logger;
|
||||
|
||||
public MediaController(
|
||||
@@ -89,7 +91,8 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
IMediaFileSystem mediaFileSystem,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IImageUrlGenerator imageUrlGenerator,
|
||||
IJsonSerializer serializer)
|
||||
IJsonSerializer serializer,
|
||||
IAuthorizationService authorizationService)
|
||||
: base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, serializer)
|
||||
{
|
||||
_shortStringHelper = shortStringHelper;
|
||||
@@ -110,6 +113,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
_logger = loggerFactory.CreateLogger<MediaController>();
|
||||
_imageUrlGenerator = imageUrlGenerator;
|
||||
_serializer = serializer;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -167,7 +171,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
|
||||
[EnsureUserPermissionForMedia("id")]
|
||||
[Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)]
|
||||
[DetermineAmbiguousActionByPassingParameters]
|
||||
public MediaItemDisplay GetById(int id)
|
||||
{
|
||||
@@ -188,7 +192,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
|
||||
[EnsureUserPermissionForMedia("id")]
|
||||
[Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)]
|
||||
[DetermineAmbiguousActionByPassingParameters]
|
||||
public MediaItemDisplay GetById(Guid id)
|
||||
{
|
||||
@@ -209,7 +213,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
|
||||
[EnsureUserPermissionForMedia("id")]
|
||||
[Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)]
|
||||
[DetermineAmbiguousActionByPassingParameters]
|
||||
public MediaItemDisplay GetById(Udi id)
|
||||
{
|
||||
@@ -432,7 +436,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[EnsureUserPermissionForMedia("id")]
|
||||
[Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)]
|
||||
[HttpPost]
|
||||
public IActionResult DeleteById(int id)
|
||||
{
|
||||
@@ -473,9 +477,16 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <param name="move"></param>
|
||||
/// <returns></returns>
|
||||
[EnsureUserPermissionForMedia("move.Id")]
|
||||
public IActionResult PostMove(MoveOrCopy move)
|
||||
public async Task<IActionResult> PostMove(MoveOrCopy move)
|
||||
{
|
||||
// Authorize...
|
||||
var requirement = new MediaPermissionResourceRequirement();
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, _mediaService.GetById(move.Id), requirement);
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var toMove = ValidateMoveOrCopy(move);
|
||||
var destinationParentID = move.ParentId;
|
||||
var sourceParentID = toMove.ParentId;
|
||||
@@ -610,8 +621,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <param name="sorted"></param>
|
||||
/// <returns></returns>
|
||||
[EnsureUserPermissionForMedia("sorted.ParentId")]
|
||||
public IActionResult PostSort(ContentSortOrder sorted)
|
||||
public async Task<IActionResult> PostSort(ContentSortOrder sorted)
|
||||
{
|
||||
if (sorted == null)
|
||||
{
|
||||
@@ -624,14 +634,21 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
return Ok();
|
||||
}
|
||||
|
||||
var mediaService = _mediaService;
|
||||
// Authorize...
|
||||
var requirement = new MediaPermissionResourceRequirement();
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, _mediaService.GetById(sorted.ParentId), requirement);
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var sortedMedia = new List<IMedia>();
|
||||
try
|
||||
{
|
||||
sortedMedia.AddRange(sorted.IdSortOrder.Select(mediaService.GetById));
|
||||
sortedMedia.AddRange(sorted.IdSortOrder.Select(_mediaService.GetById));
|
||||
|
||||
// Save Media with new sort order and update content xml in db accordingly
|
||||
if (mediaService.Sort(sortedMedia) == false)
|
||||
if (_mediaService.Sort(sortedMedia) == false)
|
||||
{
|
||||
_logger.LogWarning("Media sorting failed, this was probably caused by an event being cancelled");
|
||||
throw HttpResponseException.CreateValidationErrorResponse("Media sorting failed, this was probably caused by an event being cancelled");
|
||||
@@ -645,19 +662,16 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
public ActionResult<MediaItemDisplay> PostAddFolder(PostedFolder folder)
|
||||
public async Task<ActionResult<MediaItemDisplay>> PostAddFolder(PostedFolder folder)
|
||||
{
|
||||
var parentId = GetParentIdAsInt(folder.ParentId, validatePermissions:true);
|
||||
var parentId = await GetParentIdAsIntAsync(folder.ParentId, validatePermissions:true);
|
||||
if (!parentId.HasValue)
|
||||
{
|
||||
return NotFound("The passed id doesn't exist");
|
||||
}
|
||||
|
||||
|
||||
var mediaService = _mediaService;
|
||||
|
||||
var f = mediaService.CreateMedia(folder.Name, parentId.Value, Constants.Conventions.MediaTypes.Folder);
|
||||
mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
|
||||
var f = _mediaService.CreateMedia(folder.Name, parentId.Value, Constants.Conventions.MediaTypes.Folder);
|
||||
_mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
|
||||
|
||||
return _umbracoMapper.Map<MediaItemDisplay>(f);
|
||||
}
|
||||
@@ -682,13 +696,13 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
|
||||
//get the string json from the request
|
||||
var parentId = GetParentIdAsInt(currentFolder, validatePermissions: true);
|
||||
var parentId = await GetParentIdAsIntAsync(currentFolder, validatePermissions: true);
|
||||
if (!parentId.HasValue)
|
||||
{
|
||||
return NotFound("The passed id doesn't exist");
|
||||
}
|
||||
var tempFiles = new PostedFiles();
|
||||
var mediaService = _mediaService;
|
||||
|
||||
|
||||
//in case we pass a path with a folder in it, we will create it and upload media to it.
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
@@ -706,18 +720,18 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
{
|
||||
//look for matching folder
|
||||
folderMediaItem =
|
||||
mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder);
|
||||
_mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder);
|
||||
if (folderMediaItem == null)
|
||||
{
|
||||
//if null, create a folder
|
||||
folderMediaItem = mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder);
|
||||
mediaService.Save(folderMediaItem);
|
||||
folderMediaItem = _mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder);
|
||||
_mediaService.Save(folderMediaItem);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//get current parent
|
||||
var mediaRoot = mediaService.GetById(parentId.Value);
|
||||
var mediaRoot = _mediaService.GetById(parentId.Value);
|
||||
|
||||
//if the media root is null, something went wrong, we'll abort
|
||||
if (mediaRoot == null)
|
||||
@@ -731,8 +745,8 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
if (folderMediaItem == null)
|
||||
{
|
||||
//if null, create a folder
|
||||
folderMediaItem = mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder);
|
||||
mediaService.Save(folderMediaItem);
|
||||
folderMediaItem = _mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder);
|
||||
_mediaService.Save(folderMediaItem);
|
||||
}
|
||||
}
|
||||
//set the media root to the folder id so uploaded files will end there.
|
||||
@@ -765,7 +779,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
|
||||
var mediaItemName = fileName.ToFriendlyName();
|
||||
|
||||
var f = mediaService.CreateMedia(mediaItemName, parentId.Value, mediaType, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
|
||||
var f = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaType, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
|
||||
|
||||
|
||||
await using (var stream = formFile.OpenReadStream())
|
||||
@@ -774,7 +788,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
|
||||
|
||||
var saveResult = mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
|
||||
var saveResult = _mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
|
||||
if (saveResult == false)
|
||||
{
|
||||
AddCancelMessage(tempFiles,
|
||||
@@ -830,7 +844,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// and if that check fails an unauthorized exception will occur
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
private int? GetParentIdAsInt(string parentId, bool validatePermissions)
|
||||
private async Task<int?> GetParentIdAsIntAsync(string parentId, bool validatePermissions)
|
||||
{
|
||||
int intParentId;
|
||||
|
||||
@@ -863,22 +877,23 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
// Authorize...
|
||||
//ensure the user has access to this folder by parent id!
|
||||
if (validatePermissions && CheckPermissions(
|
||||
new Dictionary<object, object>(),
|
||||
_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser,
|
||||
_mediaService,
|
||||
_entityService,
|
||||
intParentId) == false)
|
||||
if (validatePermissions)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
var requirement = new MediaPermissionResourceRequirement();
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, _mediaService.GetById(intParentId), requirement);
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
HttpStatusCode.Forbidden,
|
||||
new SimpleNotificationModel(new BackOfficeNotification(
|
||||
_localizedTextService.Localize("speechBubbles/operationFailedHeader"),
|
||||
_localizedTextService.Localize("speechBubbles/invalidUserPermissionsText"),
|
||||
NotificationStyle.Warning)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return intParentId;
|
||||
}
|
||||
|
||||
@@ -894,8 +909,8 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
var mediaService = _mediaService;
|
||||
var toMove = mediaService.GetById(model.Id);
|
||||
|
||||
var toMove = _mediaService.GetById(model.Id);
|
||||
if (toMove == null)
|
||||
{
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
@@ -914,7 +929,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var parent = mediaService.GetById(model.ParentId);
|
||||
var parent = _mediaService.GetById(model.ParentId);
|
||||
if (parent == null)
|
||||
{
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
@@ -942,45 +957,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
return toMove;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a permissions check for the user to check if it has access to the node based on
|
||||
/// start node and/or permissions for the node
|
||||
/// </summary>
|
||||
/// <param name="storage">The storage to add the content item to so it can be reused</param>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="mediaService"></param>
|
||||
/// <param name="entityService"></param>
|
||||
/// <param name="nodeId">The content to lookup, if the contentItem is not specified</param>
|
||||
/// <param name="media">Specifies the already resolved content item to check against, setting this ignores the nodeId</param>
|
||||
/// <returns></returns>
|
||||
internal static bool CheckPermissions(IDictionary<object, object> storage, IUser user, IMediaService mediaService, IEntityService entityService, int nodeId, IMedia media = null)
|
||||
{
|
||||
if (storage == null) throw new ArgumentNullException("storage");
|
||||
if (user == null) throw new ArgumentNullException("user");
|
||||
if (mediaService == null) throw new ArgumentNullException("mediaService");
|
||||
if (entityService == null) throw new ArgumentNullException("entityService");
|
||||
|
||||
if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia)
|
||||
{
|
||||
media = mediaService.GetById(nodeId);
|
||||
//put the content item into storage so it can be retrieved
|
||||
// in the controller (saves a lookup)
|
||||
storage[typeof(IMedia).ToString()] = media;
|
||||
}
|
||||
|
||||
if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia)
|
||||
{
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
var hasPathAccess = (nodeId == Constants.System.Root)
|
||||
? user.HasMediaRootAccess(entityService)
|
||||
: (nodeId == Constants.System.RecycleBinMedia)
|
||||
? user.HasMediaBinAccess(entityService)
|
||||
: user.HasPathAccess(media, entityService);
|
||||
|
||||
return hasPathAccess;
|
||||
}
|
||||
|
||||
|
||||
public PagedResult<EntityBasic> GetPagedReferences(int id, string entityType, int pageNumber = 1, int pageSize = 100)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user