Cleans up how the content path permissions are checked, integrates the bulk publishing into the controller, adds security check to ensure the user can publish the whole branch, updates the UI messaging so they are grouped by publish statuses and deals with multiple publish status (i.e. in bulk), fixes some content tree issues

This commit is contained in:
Shannon
2018-11-15 15:24:09 +11:00
parent 5135cc93ac
commit 496ecf5c9a
17 changed files with 583 additions and 323 deletions

View File

@@ -9,6 +9,7 @@ using Umbraco.Core.Composing;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
using Umbraco.Core.Security;
namespace Umbraco.Core.Models
{
@@ -146,68 +147,46 @@ namespace Umbraco.Core.Models
internal static bool HasContentRootAccess(this IUser user, IEntityService entityService)
{
return HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
return ContentPermissionsHelper.HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
}
internal static bool HasContentBinAccess(this IUser user, IEntityService entityService)
{
return HasPathAccess(Constants.System.RecycleBinContent.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinContent.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
}
internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService)
{
return HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
return ContentPermissionsHelper.HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
}
internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService)
{
return HasPathAccess(Constants.System.RecycleBinMedia.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinMedia.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
}
internal static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService)
{
return HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
if (content == null) throw new ArgumentNullException(nameof(content));
return ContentPermissionsHelper.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
}
internal static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService)
{
return HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
if (media == null) throw new ArgumentNullException(nameof(media));
return ContentPermissionsHelper.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
}
internal static bool HasPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId)
internal static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService)
{
switch (recycleBinId)
{
case Constants.System.RecycleBinMedia:
return HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), recycleBinId);
case Constants.System.RecycleBinContent:
return HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), recycleBinId);
default:
throw new NotSupportedException("Path access is only determined on content or media");
}
if (entity == null) throw new ArgumentNullException(nameof(entity));
return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
}
internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId)
internal static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService)
{
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
// check for no access
if (startNodeIds.Length == 0)
return false;
// check for root access
if (startNodeIds.Contains(Constants.System.Root))
return true;
var formattedPath = string.Concat(",", path, ",");
// only users with root access have access to the recycle bin,
// if the above check didn't pass then access is denied
if (formattedPath.Contains(string.Concat(",", recycleBinId, ",")))
return false;
// check for a start node in the path
return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x, ",")));
if (entity == null) throw new ArgumentNullException(nameof(entity));
return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
}
internal static bool IsInBranchOfStartNode(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId, out bool hasPathAccess)
@@ -215,58 +194,14 @@ namespace Umbraco.Core.Models
switch (recycleBinId)
{
case Constants.System.RecycleBinMedia:
return IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess);
return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess);
case Constants.System.RecycleBinContent:
return IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess);
return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess);
default:
throw new NotSupportedException("Path access is only determined on content or media");
}
}
internal static bool IsInBranchOfStartNode(string path, int[] startNodeIds, string[] startNodePaths, out bool hasPathAccess)
{
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
hasPathAccess = false;
// check for no access
if (startNodeIds.Length == 0)
return false;
// check for root access
if (startNodeIds.Contains(Constants.System.Root))
{
hasPathAccess = true;
return true;
}
//is it self?
var self = startNodePaths.Any(x => x == path);
if (self)
{
hasPathAccess = true;
return true;
}
//is it ancestor?
var ancestor = startNodePaths.Any(x => x.StartsWith(path));
if (ancestor)
{
//hasPathAccess = false;
return true;
}
//is it descendant?
var descendant = startNodePaths.Any(x => path.StartsWith(x));
if (descendant)
{
hasPathAccess = true;
return true;
}
return false;
}
/// <summary>
/// Determines whether this user has access to view sensitive data
/// </summary>

View File

@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
namespace Umbraco.Core.Security
{
internal class ContentPermissionsHelper
{
public enum ContentAccess
{
Granted,
Denied,
NotFound
}
public static ContentAccess CheckPermissions(
IContent content,
IUser user,
IUserService userService,
IEntityService entityService,
params char[] permissionsToCheck)
{
if (user == null) throw new ArgumentNullException("user");
if (userService == null) throw new ArgumentNullException("userService");
if (entityService == null) throw new ArgumentNullException("entityService");
if (content == null) return ContentAccess.NotFound;
var hasPathAccess = user.HasPathAccess(content, entityService);
if (hasPathAccess == false)
return ContentAccess.Denied;
if (permissionsToCheck == null || permissionsToCheck.Length == 0)
return ContentAccess.Granted;
//get the implicit/inherited permissions for the user for this path
return CheckPermissionsPath(content.Path, user, userService, permissionsToCheck)
? ContentAccess.Granted
: ContentAccess.Denied;
}
public static ContentAccess CheckPermissions(
IUmbracoEntity entity,
IUser user,
IUserService userService,
IEntityService entityService,
params char[] permissionsToCheck)
{
if (user == null) throw new ArgumentNullException("user");
if (userService == null) throw new ArgumentNullException("userService");
if (entityService == null) throw new ArgumentNullException("entityService");
if (entity == null) return ContentAccess.NotFound;
var hasPathAccess = user.HasContentPathAccess(entity, entityService);
if (hasPathAccess == false)
return ContentAccess.Denied;
if (permissionsToCheck == null || permissionsToCheck.Length == 0)
return ContentAccess.Granted;
//get the implicit/inherited permissions for the user for this path
return CheckPermissionsPath(entity.Path, user, userService, permissionsToCheck)
? ContentAccess.Granted
: ContentAccess.Denied;
}
/// <summary>
/// Checks if the user has access to the specified node and permissions set
/// </summary>
/// <param name="nodeId"></param>
/// <param name="user"></param>
/// <param name="userService"></param>
/// <param name="entityService"></param>
/// <param name="entity">The <see cref="IUmbracoEntity"/> item resolved if one was found for the id</param>
/// <param name="permissionsToCheck"></param>
/// <returns></returns>
public static ContentAccess CheckPermissions(
int nodeId,
IUser user,
IUserService userService,
IEntityService entityService,
out IUmbracoEntity entity,
params char[] permissionsToCheck)
{
if (user == null) throw new ArgumentNullException("user");
if (userService == null) throw new ArgumentNullException("userService");
if (entityService == null) throw new ArgumentNullException("entityService");
bool? hasPathAccess = null;
entity = null;
if (nodeId == Constants.System.Root)
hasPathAccess = user.HasContentRootAccess(entityService);
else if (nodeId == Constants.System.RecycleBinContent)
hasPathAccess = user.HasContentBinAccess(entityService);
if (hasPathAccess.HasValue)
return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied;
entity = entityService.Get(nodeId, UmbracoObjectTypes.Document);
if (entity == null) return ContentAccess.NotFound;
hasPathAccess = user.HasContentPathAccess(entity, entityService);
if (hasPathAccess == false)
return ContentAccess.Denied;
if (permissionsToCheck == null || permissionsToCheck.Length == 0)
return ContentAccess.Granted;
//get the implicit/inherited permissions for the user for this path
return CheckPermissionsPath(entity.Path, user, userService, permissionsToCheck)
? ContentAccess.Granted
: ContentAccess.Denied;
}
/// <summary>
/// Checks if the user has access to the specified node and permissions set
/// </summary>
/// <param name="nodeId"></param>
/// <param name="user"></param>
/// <param name="userService"></param>
/// <param name="contentService"></param>
/// <param name="entityService"></param>
/// <param name="contentItem">The <see cref="IContent"/> item resolved if one was found for the id</param>
/// <param name="permissionsToCheck"></param>
/// <returns></returns>
public static ContentAccess CheckPermissions(
int nodeId,
IUser user,
IUserService userService,
IContentService contentService,
IEntityService entityService,
out IContent contentItem,
params char[] permissionsToCheck)
{
if (user == null) throw new ArgumentNullException("user");
if (userService == null) throw new ArgumentNullException("userService");
if (contentService == null) throw new ArgumentNullException("contentService");
if (entityService == null) throw new ArgumentNullException("entityService");
bool? hasPathAccess = null;
contentItem = null;
if (nodeId == Constants.System.Root)
hasPathAccess = user.HasContentRootAccess(entityService);
else if (nodeId == Constants.System.RecycleBinContent)
hasPathAccess = user.HasContentBinAccess(entityService);
if (hasPathAccess.HasValue)
return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied;
contentItem = contentService.GetById(nodeId);
if (contentItem == null) return ContentAccess.NotFound;
hasPathAccess = user.HasPathAccess(contentItem, entityService);
if (hasPathAccess == false)
return ContentAccess.Denied;
if (permissionsToCheck == null || permissionsToCheck.Length == 0)
return ContentAccess.Granted;
//get the implicit/inherited permissions for the user for this path
return CheckPermissionsPath(contentItem.Path, user, userService, permissionsToCheck)
? ContentAccess.Granted
: ContentAccess.Denied;
}
private static bool CheckPermissionsPath(string path, IUser user, IUserService userService, params char[] permissionsToCheck)
{
//get the implicit/inherited permissions for the user for this path,
//if there is no content item for this id, than just use the id as the path (i.e. -1 or -20)
var permission = userService.GetPermissionsForPath(user, path);
var allowed = true;
foreach (var p in permissionsToCheck)
{
if (permission == null
|| permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false)
{
allowed = false;
}
}
return allowed;
}
public static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId)
{
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
// check for no access
if (startNodeIds.Length == 0)
return false;
// check for root access
if (startNodeIds.Contains(Constants.System.Root))
return true;
var formattedPath = string.Concat(",", path, ",");
// only users with root access have access to the recycle bin,
// if the above check didn't pass then access is denied
if (formattedPath.Contains(string.Concat(",", recycleBinId, ",")))
return false;
// check for a start node in the path
return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x, ",")));
}
internal static bool IsInBranchOfStartNode(string path, int[] startNodeIds, string[] startNodePaths, out bool hasPathAccess)
{
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
hasPathAccess = false;
// check for no access
if (startNodeIds.Length == 0)
return false;
// check for root access
if (startNodeIds.Contains(Constants.System.Root))
{
hasPathAccess = true;
return true;
}
//is it self?
var self = startNodePaths.Any(x => x == path);
if (self)
{
hasPathAccess = true;
return true;
}
//is it ancestor?
var ancestor = startNodePaths.Any(x => x.StartsWith(path));
if (ancestor)
{
//hasPathAccess = false;
return true;
}
//is it descendant?
var descendant = startNodePaths.Any(x => path.StartsWith(x));
if (descendant)
{
hasPathAccess = true;
return true;
}
return false;
}
}
}

View File

@@ -1287,6 +1287,7 @@
<Compile Include="Security\AuthenticationExtensions.cs" />
<Compile Include="Security\BackOfficeUserStore.cs" />
<Compile Include="Security\BackOfficeUserValidator.cs" />
<Compile Include="Security\ContentPermissionsHelper.cs" />
<Compile Include="Security\EmailService.cs" />
<Compile Include="Security\IMembershipProviderPasswordHasher.cs" />
<Compile Include="Security\IUmbracoMemberTypeMembershipProvider.cs" />

View File

@@ -5,6 +5,7 @@ using NUnit.Framework;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Web.Editors;
@@ -33,14 +34,14 @@ namespace Umbraco.Tests.Web.Controllers
var userService = userServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, 1234);
var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent);
//assert
Assert.IsTrue(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result);
}
[Test]
public void Throws_Exception_When_No_Content_Found()
public void No_Content_Found()
{
//arrange
var userMock = new Mock<IUser>();
@@ -60,8 +61,11 @@ namespace Umbraco.Tests.Web.Controllers
var entityServiceMock = new Mock<IEntityService>();
var entityService = entityServiceMock.Object;
//act/assert
Assert.Throws<HttpResponseException>(() => ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, 1234, new[] { 'F' }));
//act
var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' });
//assert
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.NotFound, result);
}
[Test]
@@ -89,10 +93,10 @@ namespace Umbraco.Tests.Web.Controllers
var entityService = entityServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, 1234, new[] { 'F' });
var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' });
//assert
Assert.IsFalse(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result);
}
[Test]
@@ -120,10 +124,10 @@ namespace Umbraco.Tests.Web.Controllers
var entityService = entityServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, 1234, new[] { 'F' });
var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' });
//assert
Assert.IsFalse(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result);
}
[Test]
@@ -152,10 +156,10 @@ namespace Umbraco.Tests.Web.Controllers
var entityService = entityServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, 1234, new[] { 'F' });
var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' });
//assert
Assert.IsTrue(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result);
}
[Test]
@@ -174,10 +178,10 @@ namespace Umbraco.Tests.Web.Controllers
var entityService = entityServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, -1);
var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent);
//assert
Assert.IsTrue(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result);
}
[Test]
@@ -196,10 +200,10 @@ namespace Umbraco.Tests.Web.Controllers
var entityService = entityServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, -20);
var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent);
//assert
Assert.IsTrue(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result);
}
[Test]
@@ -220,10 +224,10 @@ namespace Umbraco.Tests.Web.Controllers
var entityService = entityServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, -20);
var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent);
//assert
Assert.IsFalse(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result);
}
[Test]
@@ -244,10 +248,10 @@ namespace Umbraco.Tests.Web.Controllers
var entityService = entityServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, -1);
var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent);
//assert
Assert.IsFalse(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result);
}
[Test]
@@ -274,10 +278,10 @@ namespace Umbraco.Tests.Web.Controllers
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, -1, new[] { 'A' });
var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent, new[] { 'A' });
//assert
Assert.IsTrue(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result);
}
[Test]
@@ -302,10 +306,10 @@ namespace Umbraco.Tests.Web.Controllers
var contentService = contentServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, -1, new[] { 'B' });
var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent, new[] { 'B' });
//assert
Assert.IsFalse(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result);
}
[Test]
@@ -332,10 +336,10 @@ namespace Umbraco.Tests.Web.Controllers
var contentService = contentServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, -20, new[] { 'A' });
var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent, new[] { 'A' });
//assert
Assert.IsTrue(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result);
}
[Test]
@@ -360,10 +364,10 @@ namespace Umbraco.Tests.Web.Controllers
var contentService = contentServiceMock.Object;
//act
var result = ContentController.CheckPermissions(new Dictionary<string, object>(), user, userService, contentService, entityService, -20, new[] { 'B' });
var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent, new[] { 'B' });
//assert
Assert.IsFalse(result);
Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result);
}
}

View File

@@ -6,7 +6,6 @@
var vm = this;
vm.changeSelection = changeSelection;
vm.includeUnpublishedChanged = includeUnpublishedChanged;
function onInit() {

View File

@@ -1158,6 +1158,10 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="paSimpleHelp">If you just want to setup simple protection using a single login and password</key>
</area>
<area alias="publish">
<key alias="invalidPublishBranchPermissions">Insufficient user permissions to publish all descendant documents</key>
<key alias="contentPublishedFailedIsTrashed"><![CDATA[
%0% could not be published because the item is in the recycle bin.
]]></key>
<key alias="contentPublishedFailedAwaitingRelease"><![CDATA[
%0% could not be published because the item is scheduled for release.
]]></key>
@@ -1165,7 +1169,7 @@ To manage your website, simply open the Umbraco back office and start adding con
%0% could not be published because the item has expired.
]]></key>
<key alias="contentPublishedFailedInvalid"><![CDATA[
%0% could not be published because these properties: %1% did not pass validation rules.
%0% could not be published because some properties did not pass validation rules.
]]></key>
<key alias="contentPublishedFailedByEvent"><![CDATA[
%0% could not be published, a 3rd party add-in cancelled the action.
@@ -1272,7 +1276,6 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="invalidUserPermissionsText">Insufficient user permissions, could not complete the operation</key>
<key alias="operationCancelledHeader">Cancelled</key>
<key alias="operationCancelledText">Operation was cancelled by a 3rd party add-in</key>
<key alias="contentPublishedFailedByEvent">Publishing was cancelled by a 3rd party add-in</key>
<key alias="contentTypeDublicatePropertyType">Property type already exists</key>
<key alias="contentTypePropertyTypeCreated">Property type created</key>
<key alias="contentTypePropertyTypeCreatedText"><![CDATA[Name: %0% <br /> DataType: %1%]]></key>
@@ -1286,10 +1289,11 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="cssSavedText">Stylesheet saved without any errors</key>
<key alias="dataTypeSaved">Datatype saved</key>
<key alias="dictionaryItemSaved">Dictionary item saved</key>
<key alias="editContentPublishedFailedByParent">Publishing failed because the parent page isn't published</key>
<key alias="editContentPublishedHeader">Content published</key>
<key alias="editContentPublishedText">and is visible on the website</key>
<key alias="editMultiContentPublishedText">%0% documents published and visible on the website</key>
<key alias="editVariantPublishedText">%0% published and visible on the website</key>
<key alias="editMultiVariantPublishedText">%0% documents published for languages %1% and visible on the website</key>
<key alias="editContentSavedHeader">Content saved</key>
<key alias="editContentSavedText">Remember to publish to make changes visible</key>
<key alias="editContentScheduledSavedText">A schedule for publishing has been updated</key>

View File

@@ -38,7 +38,8 @@ using Umbraco.Web.Actions;
using Umbraco.Web.ContentApps;
using Umbraco.Web.Editors.Binders;
using Umbraco.Web.Editors.Filters;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Security;
namespace Umbraco.Web.Editors
{
@@ -738,6 +739,27 @@ namespace Umbraco.Web.Editors
case ContentSaveAction.PublishNew:
{
var publishStatus = PublishInternal(contentItem, out wasCancelled, out var successfulCultures);
//global notifications
AddMessageForPublishStatus(new[] { publishStatus }, globalNotifications, successfulCultures);
//variant specific notifications
foreach (var c in successfulCultures)
AddMessageForPublishStatus(new[] { publishStatus }, notifications.GetOrCreate(c), successfulCultures);
}
break;
case ContentSaveAction.PublishWithDescendants:
case ContentSaveAction.PublishWithDescendantsNew:
{
if (!ValidatePublishBranchPermissions(contentItem, out var noAccess))
{
globalNotifications.AddErrorNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/invalidPublishBranchPermissions"));
wasCancelled = false;
break;
}
var publishStatus = PublishBranchInternal(contentItem, false, out wasCancelled, out var successfulCultures);
//global notifications
AddMessageForPublishStatus(publishStatus, globalNotifications, successfulCultures);
//variant specific notifications
@@ -745,30 +767,25 @@ namespace Umbraco.Web.Editors
AddMessageForPublishStatus(publishStatus, notifications.GetOrCreate(c), successfulCultures);
}
break;
case ContentSaveAction.PublishWithDescendants:
case ContentSaveAction.PublishWithDescendantsNew:
{
var publishStatus = PublishBranchInternal(contentItem, false, out wasCancelled, out var successfulCultures);
//TODO: Deal with the multiple result
////global notifications
//AddMessageForPublishStatus(publishStatus, globalNotifications, successfulCultures);
////variant specific notifications
//foreach (var c in successfulCultures)
// AddMessageForPublishStatus(publishStatus, notifications.GetOrCreate(c), successfulCultures);
}
break;
case ContentSaveAction.PublishWithDescendantsForce:
case ContentSaveAction.PublishWithDescendantsForceNew:
{
if (!ValidatePublishBranchPermissions(contentItem, out var noAccess))
{
globalNotifications.AddErrorNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/invalidPublishBranchPermissions"));
wasCancelled = false;
break;
}
var publishStatus = PublishBranchInternal(contentItem, true, out wasCancelled, out var successfulCultures);
//TODO: Deal with the multiple result
////global notifications
//AddMessageForPublishStatus(publishStatus, globalNotifications, successfulCultures);
////variant specific notifications
//foreach (var c in successfulCultures)
// AddMessageForPublishStatus(publishStatus, notifications.GetOrCreate(c), successfulCultures);
//global notifications
AddMessageForPublishStatus(publishStatus, globalNotifications, successfulCultures);
//variant specific notifications
foreach (var c in successfulCultures)
AddMessageForPublishStatus(publishStatus, notifications.GetOrCreate(c), successfulCultures);
}
break;
default:
@@ -1039,6 +1056,40 @@ namespace Umbraco.Web.Editors
notifications.GetOrCreate(culture).AddSuccessNotification(header, msg);
}
/// <summary>
/// The user must have publish access to all descendant nodes of the content item in order to continue
/// </summary>
/// <param name="contentItem"></param>
/// <returns></returns>
private bool ValidatePublishBranchPermissions(ContentItemSave contentItem, out IReadOnlyList<IUmbracoEntity> noAccess)
{
var denied = new List<IUmbracoEntity>();
var page = 0;
const int pageSize = 500;
var total = long.MaxValue;
while (page * pageSize < total)
{
var descendants = Services.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
"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,
Security.CurrentUser, Services.UserService, Services.EntityService,
ActionPublish.ActionLetter) == ContentPermissionsHelper.ContentAccess.Denied))
{
denied.Add(c);
}
}
}
noAccess = denied;
return denied.Count == 0;
}
private IEnumerable<PublishResult> PublishBranchInternal(ContentItemSave contentItem, bool force,
out bool wasCancelled, out string[] successfulCultures)
{
@@ -1269,7 +1320,7 @@ namespace Umbraco.Web.Editors
if (publishResult.Success == false)
{
var notificationModel = new SimpleNotificationModel();
AddMessageForPublishStatus(publishResult, notificationModel);
AddMessageForPublishStatus(new [] { publishResult }, notificationModel);
return Request.CreateValidationErrorResponse(notificationModel);
}
@@ -1817,152 +1868,124 @@ namespace Umbraco.Web.Editors
/// <param name="successfulCultures">
/// This is null when dealing with invariant content, else it's the cultures that were succesfully published
/// </param>
private void AddMessageForPublishStatus(PublishResult status, INotificationModel display, string[] successfulCultures = null)
private void AddMessageForPublishStatus(IEnumerable<PublishResult> statuses, INotificationModel display, string[] successfulCultures = null)
{
switch (status.Result)
//Put the statuses into groups, each group results in a different message
var statusGroup = statuses.GroupBy(x =>
{
case PublishResultType.SuccessPublish:
case PublishResultType.SuccessPublishAlready:
case PublishResultType.SuccessPublishCulture:
if (successfulCultures == null)
{
display.AddSuccessNotification(
Services.TextService.Localize("speechBubbles/editContentPublishedHeader"),
Services.TextService.Localize("speechBubbles/editContentPublishedText"));
}
else
{
foreach (var c in successfulCultures)
switch (x.Result)
{
case PublishResultType.SuccessPublish:
case PublishResultType.SuccessPublishAlready:
case PublishResultType.SuccessPublishCulture:
//these 3 belong to a single group
return PublishResultType.SuccessPublish;
case PublishResultType.FailedPublishAwaitingRelease:
case PublishResultType.FailedPublishCultureAwaitingRelease:
//these 2 belong to a single group
return PublishResultType.FailedPublishAwaitingRelease;
case PublishResultType.FailedPublishHasExpired:
case PublishResultType.FailedPublishCultureHasExpired:
//these 2 belong to a single group
return PublishResultType.FailedPublishHasExpired;
case PublishResultType.FailedPublishPathNotPublished:
case PublishResultType.FailedPublishCancelledByEvent:
case PublishResultType.FailedPublishIsTrashed:
case PublishResultType.FailedPublishContentInvalid:
case PublishResultType.FailedPublishMandatoryCultureMissing:
//the rest that we are looking for each belong in their own group
return x.Result;
default:
throw new IndexOutOfRangeException($"{x.Result}\" was not expected.");
}
});
foreach (var status in statusGroup)
{
switch (status.Key)
{
case PublishResultType.SuccessPublish:
var itemCount = status.Count();
if (successfulCultures == null)
{
display.AddSuccessNotification(
Services.TextService.Localize("speechBubbles/editContentPublishedHeader"),
Services.TextService.Localize("speechBubbles/editVariantPublishedText", new[] { _allLangs.Value[c].CultureName }));
itemCount > 1
? Services.TextService.Localize("speechBubbles/editContentPublishedText")
: Services.TextService.Localize("speechBubbles/editMultiContentPublishedText", new[] { itemCount.ToInvariantString() }));
}
}
break;
case PublishResultType.FailedPublishPathNotPublished:
display.AddWarningNotification(
else
{
foreach (var c in successfulCultures)
{
display.AddSuccessNotification(
Services.TextService.Localize("speechBubbles/editContentPublishedHeader"),
itemCount > 1
? Services.TextService.Localize("speechBubbles/editMultiVariantPublishedText", new[] { itemCount.ToInvariantString(), _allLangs.Value[c].CultureName })
: Services.TextService.Localize("speechBubbles/editVariantPublishedText", new[] { _allLangs.Value[c].CultureName }));
}
}
break;
case PublishResultType.FailedPublishPathNotPublished:
{
var names = string.Join(", ", status.Select(x => $"{x.Content.Name} ({x.Content.Id})"));
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedByParent",
new[] { names }).Trim());
}
break;
case PublishResultType.FailedPublishCancelledByEvent:
{
var names = string.Join(", ", status.Select(x => $"{x.Content.Name} ({x.Content.Id})"));
AddCancelMessage(display, message: "publish/contentPublishedFailedByEvent", messageParams: new[] { names });
}
break;
case PublishResultType.FailedPublishAwaitingRelease:
{
var names = string.Join(", ", status.Select(x => $"{x.Content.Name} ({x.Content.Id})"));
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease",
new[] { names }).Trim());
}
break;
case PublishResultType.FailedPublishHasExpired:
{
var names = string.Join(", ", status.Select(x => $"{x.Content.Name} ({x.Content.Id})"));
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedExpired",
new[] { names }).Trim());
}
break;
case PublishResultType.FailedPublishIsTrashed:
{
var names = string.Join(", ", status.Select(x => $"{x.Content.Name} ({x.Content.Id})"));
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedIsTrashed",
new[] { names }).Trim());
}
break;
case PublishResultType.FailedPublishContentInvalid:
{
var names = string.Join(", ", status.Select(x => $"{x.Content.Name} ({x.Content.Id})"));
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedInvalid",
new[] { names }).Trim());
}
break;
case PublishResultType.FailedPublishMandatoryCultureMissing:
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedByParent",
new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim());
break;
case PublishResultType.FailedPublishCancelledByEvent:
AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent");
break;
case PublishResultType.FailedPublishAwaitingRelease:
case PublishResultType.FailedPublishCultureAwaitingRelease:
//TODO: We'll need to deal with variants here eventually
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease",
new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim());
break;
case PublishResultType.FailedPublishHasExpired:
case PublishResultType.FailedPublishCultureHasExpired:
//TODO: We'll need to deal with variants here eventually
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedExpired",
new[] { $"{status.Content.Name} ({status.Content.Id})", }).Trim());
break;
case PublishResultType.FailedPublishIsTrashed:
display.AddWarningNotification(
Services.TextService.Localize("publish"),
"publish/contentPublishedFailedIsTrashed"); // fixme properly localize, these keys are missing from lang files!
break;
case PublishResultType.FailedPublishContentInvalid:
display.AddWarningNotification(
Services.TextService.Localize("publish"),
Services.TextService.Localize("publish/contentPublishedFailedInvalid",
new[]
{
$"{status.Content.Name} ({status.Content.Id})",
string.Join(",", status.InvalidProperties.Select(x => x.Alias))
}).Trim());
break;
case PublishResultType.FailedPublishMandatoryCultureMissing:
display.AddWarningNotification(
Services.TextService.Localize("publish"),
"publish/contentPublishedFailedByCulture"); // fixme properly localize, these keys are missing from lang files!
break;
default:
throw new IndexOutOfRangeException($"PublishedResultType \"{status.Result}\" was not expected.");
}
}
/// <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="userService"></param>
/// <param name="contentService"></param>
/// <param name="entityService"></param>
/// <param name="nodeId">The content to lookup, if the contentItem is not specified</param>
/// <param name="permissionsToCheck"></param>
/// <param name="contentItem">Specifies the already resolved content item to check against</param>
/// <returns></returns>
internal static bool CheckPermissions(
IDictionary<string, object> storage,
IUser user,
IUserService userService,
IContentService contentService,
IEntityService entityService,
int nodeId,
char[] permissionsToCheck = null,
IContent contentItem = null)
{
if (storage == null) throw new ArgumentNullException("storage");
if (user == null) throw new ArgumentNullException("user");
if (userService == null) throw new ArgumentNullException("userService");
if (contentService == null) throw new ArgumentNullException("contentService");
if (entityService == null) throw new ArgumentNullException("entityService");
if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent)
{
contentItem = contentService.GetById(nodeId);
//put the content item into storage so it can be retreived
// in the controller (saves a lookup)
storage[typeof(IContent).ToString()] = contentItem;
}
if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
var hasPathAccess = (nodeId == Constants.System.Root)
? user.HasContentRootAccess(entityService)
: (nodeId == Constants.System.RecycleBinContent)
? user.HasContentBinAccess(entityService)
: user.HasPathAccess(contentItem, entityService);
if (hasPathAccess == false)
{
return false;
}
if (permissionsToCheck == null || permissionsToCheck.Length == 0)
{
return true;
}
//get the implicit/inherited permissions for the user for this path,
//if there is no content item for this id, than just use the id as the path (i.e. -1 or -20)
var path = contentItem != null ? contentItem.Path : nodeId.ToString();
var permission = userService.GetPermissionsForPath(user, path);
var allowed = true;
foreach (var p in permissionsToCheck)
{
if (permission == null
|| permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false)
{
allowed = false;
"publish/contentPublishedFailedByCulture"); // fixme properly localize, these keys are missing from lang files!
break;
default:
throw new IndexOutOfRangeException($"PublishedResultType \"{status.Key}\" was not expected.");
}
}
return allowed;
}
/// <summary>

View File

@@ -148,7 +148,9 @@ namespace Umbraco.Web.Editors
string header = "speechBubbles/operationCancelledHeader",
string message = "speechBubbles/operationCancelledText",
bool localizeHeader = true,
bool localizeMessage = true)
bool localizeMessage = true,
string[] headerParams = null,
string[] messageParams = null)
{
//if there's already a default event message, don't add our default one
//fixme inject
@@ -156,8 +158,8 @@ namespace Umbraco.Web.Editors
if (msgs != null && msgs.GetAll().Any(x => x.IsDefaultEventMessage)) return;
display.AddWarningNotification(
localizeHeader ? Services.TextService.Localize(header) : header,
localizeMessage ? Services.TextService.Localize(message): message);
localizeHeader ? Services.TextService.Localize(header, headerParams) : header,
localizeMessage ? Services.TextService.Localize(message, messageParams): message);
}
}
}

View File

@@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Web.Actions;
using Umbraco.Web.Composing;
@@ -15,7 +17,6 @@ using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;
using Umbraco.Web.WebApi;
namespace Umbraco.Web.Editors.Filters
{
/// <summary>
@@ -100,6 +101,8 @@ namespace Umbraco.Web.Editors.Filters
contentIdToCheck = contentToCheck.Id;
break;
case ContentSaveAction.Publish:
case ContentSaveAction.PublishWithDescendants:
case ContentSaveAction.PublishWithDescendantsForce:
permissionToCheck.Add(ActionPublish.ActionLetter);
contentToCheck = contentItem.PersistedContent;
contentIdToCheck = contentToCheck.Id;
@@ -146,6 +149,8 @@ namespace Umbraco.Web.Editors.Filters
}
break;
case ContentSaveAction.PublishNew:
case ContentSaveAction.PublishWithDescendantsNew:
case ContentSaveAction.PublishWithDescendantsForceNew:
//Publish new requires both ActionNew AND ActionPublish
//TODO: Shoudn't publish also require ActionUpdate since it will definitely perform an update to publish but maybe that's just implied
@@ -182,17 +187,34 @@ namespace Umbraco.Web.Editors.Filters
throw new ArgumentOutOfRangeException();
}
if (ContentController.CheckPermissions(
actionContext.Request.Properties,
_security.CurrentUser,
_userService, _contentService, _entityService,
contentIdToCheck, permissionToCheck.ToArray(), contentToCheck) == false)
ContentPermissionsHelper.ContentAccess accessResult;
if (contentToCheck != null)
{
actionContext.Response = actionContext.Request.CreateUserNoAccessResponse();
return false;
//store the content item in request cache so it can be resolved in the controller without re-looking it up
actionContext.Request.Properties[typeof(IContent).ToString()] = contentItem;
accessResult = ContentPermissionsHelper.CheckPermissions(
contentToCheck, _security.CurrentUser,
_userService, _entityService, permissionToCheck.ToArray());
}
else
{
accessResult = ContentPermissionsHelper.CheckPermissions(
contentIdToCheck, _security.CurrentUser,
_userService, _contentService, _entityService,
out contentToCheck,
permissionToCheck.ToArray());
if (contentToCheck != null)
{
//store the content item in request cache so it can be resolved in the controller without re-looking it up
actionContext.Request.Properties[typeof(IContent).ToString()] = contentToCheck;
}
}
return true;
if (accessResult == ContentPermissionsHelper.ContentAccess.NotFound)
throw new HttpResponseException(HttpStatusCode.NotFound);
return accessResult == ContentPermissionsHelper.ContentAccess.Granted;
}
}
}

View File

@@ -720,8 +720,7 @@ namespace Umbraco.Web.Editors
if (saveResult == false)
{
AddCancelMessage(tempFiles,
message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName,
localizeMessage: false);
message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName);
}
else
{

View File

@@ -3,6 +3,7 @@ using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
namespace Umbraco.Web.Editors
@@ -113,7 +114,7 @@ namespace Umbraco.Web.Editors
{
if (contentId == Constants.System.Root)
{
var hasAccess = UserExtensions.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService), Constants.System.RecycleBinContent);
var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService), Constants.System.RecycleBinContent);
if (hasAccess == false)
return Attempt.Fail("The current user does not have access to the content root");
}
@@ -134,7 +135,7 @@ namespace Umbraco.Web.Editors
{
if (mediaId == Constants.System.Root)
{
var hasAccess = UserExtensions.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService), Constants.System.RecycleBinMedia);
var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService), Constants.System.RecycleBinMedia);
if (hasAccess == false)
return Attempt.Fail("The current user does not have access to the media root");
}

View File

@@ -65,38 +65,36 @@ namespace Umbraco.Web.Trees
queryStrings,
hasChildren);
// entity is either a container, or a document
// set container style if it is one
if (isContainer)
{
node.AdditionalData.Add("isContainer", true);
node.SetContainerStyle();
}
var documentEntity = (IDocumentEntitySlim)entity;
if (!documentEntity.Variations.VariesByCulture())
{
if (!documentEntity.Published)
node.SetNotPublishedStyle();
else if (documentEntity.Edited)
node.SetHasPendingVersionStyle();
}
else
{
var documentEntity = (IDocumentEntitySlim) entity;
if (!documentEntity.Variations.VariesByCulture())
if (!culture.IsNullOrWhiteSpace())
{
if (!documentEntity.Published)
if (!documentEntity.Published || !documentEntity.PublishedCultures.Contains(culture))
node.SetNotPublishedStyle();
else if (documentEntity.Edited)
else if (documentEntity.EditedCultures.Contains(culture))
node.SetHasPendingVersionStyle();
}
else
{
if (!culture.IsNullOrWhiteSpace())
{
if (!documentEntity.PublishedCultures.Contains(culture))
node.SetNotPublishedStyle();
else if (documentEntity.EditedCultures.Contains(culture))
node.SetHasPendingVersionStyle();
}
}
node.AdditionalData.Add("variesByCulture", documentEntity.Variations.VariesByCulture());
node.AdditionalData.Add("contentType", documentEntity.ContentTypeAlias);
}
node.AdditionalData.Add("variesByCulture", documentEntity.Variations.VariesByCulture());
node.AdditionalData.Add("contentType", documentEntity.ContentTypeAlias);
if (Services.PublicAccessService.IsProtected(entity.Path))
node.SetProtectedStyle();
@@ -161,7 +159,7 @@ namespace Umbraco.Web.Trees
}
//if the user has no path access for this node, all they can do is refresh
if (Security.CurrentUser.HasPathAccess(item, Services.EntityService, RecycleBinId) == false)
if (!Security.CurrentUser.HasContentPathAccess(item, Services.EntityService))
{
var menu = new MenuItemCollection();
menu.Items.Add(new RefreshNode(Services.TextService, true));

View File

@@ -232,7 +232,9 @@ namespace Umbraco.Web.Trees
protected bool HasPathAccess(IUmbracoEntity entity, FormDataCollection queryStrings)
{
if (entity == null) return false;
return Security.CurrentUser.HasPathAccess(entity, Services.EntityService, RecycleBinId);
return RecycleBinId == Constants.System.RecycleBinContent
? Security.CurrentUser.HasContentPathAccess(entity, Services.EntityService)
: Security.CurrentUser.HasMediaPathAccess(entity, Services.EntityService);
}
/// <summary>

View File

@@ -112,7 +112,7 @@ namespace Umbraco.Web.Trees
}
//if the user has no path access for this node, all they can do is refresh
if (Security.CurrentUser.HasPathAccess(item, Services.EntityService, RecycleBinId) == false)
if (!Security.CurrentUser.HasMediaPathAccess(item, Services.EntityService))
{
menu.Items.Add(new RefreshNode(Services.TextService, true));
return menu;

View File

@@ -54,7 +54,9 @@ namespace Umbraco.Web.UI.Pages
var entity = entityId == Constants.System.Root
? EntitySlim.Root
: Services.EntityService.Get(entityId, objectType);
var hasAccess = Security.CurrentUser.HasPathAccess(entity, Services.EntityService, objectType == UmbracoObjectTypes.Document ? Constants.System.RecycleBinContent : Constants.System.RecycleBinMedia);
var hasAccess = objectType == UmbracoObjectTypes.Document
? Security.CurrentUser.HasContentPathAccess(entity, Services.EntityService)
: Security.CurrentUser.HasMediaPathAccess(entity, Services.EntityService);
if (hasAccess == false)
throw new AuthorizationException($"The current user doesn't have access to the path '{entity.Path}'");

View File

@@ -9,6 +9,8 @@ using Umbraco.Web.Editors;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Web.Actions;
using Umbraco.Core.Security;
using System.Net;
namespace Umbraco.Web.WebApi.Filters
{
@@ -108,25 +110,27 @@ namespace Umbraco.Web.WebApi.Filters
nodeId = _nodeId.Value;
}
if (ContentController.CheckPermissions(
actionContext.Request.Properties,
//fixme: inject? we can't because this is an attribute but we could provide ctors and empty ctors that pass in the required services
var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId,
Current.UmbracoContext.Security.CurrentUser,
Current.Services.UserService,
Current.Services.ContentService,
Current.Services.EntityService,
nodeId, _permissionToCheck.HasValue ? new[]{_permissionToCheck.Value}: null))
{
base.OnActionExecuting(actionContext);
}
else
{
out var contentItem,
_permissionToCheck.HasValue ? new[] { _permissionToCheck.Value } : null);
if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound)
throw new HttpResponseException(HttpStatusCode.NotFound);
if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied)
throw new HttpResponseException(actionContext.Request.CreateUserNoAccessResponse());
if (contentItem != null)
{
//store the content item in request cache so it can be resolved in the controller without re-looking it up
actionContext.Request.Properties[typeof(IContent).ToString()] = contentItem;
}
base.OnActionExecuting(actionContext);
}
}
}

View File

@@ -8,6 +8,7 @@ using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Composing;
using Umbraco.Core.Security;
namespace Umbraco.Web.WebApi.Filters
{
@@ -79,7 +80,7 @@ namespace Umbraco.Web.WebApi.Filters
var toRemove = new List<dynamic>();
foreach (dynamic item in items)
{
var hasPathAccess = (item != null && UserExtensions.HasPathAccess(item.Path, GetUserStartNodes(user), RecycleBinId));
var hasPathAccess = (item != null && ContentPermissionsHelper.HasPathAccess(item.Path, GetUserStartNodes(user), RecycleBinId));
if (hasPathAccess == false)
{
toRemove.Add(item);