From cbc841301b31cd71770eeb9a70694cf4d8235a21 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 7 Aug 2013 19:28:32 +1000 Subject: [PATCH] Got more of the sorting stuff working, started implementing much of the back office security checks in the content controller, updates some of the c# unit tests and got a self-hosted test running (but commented out currently until we can mock things properly). --- src/Umbraco.Core/TypeHelper.cs | 2 +- .../ContentControllerHostedTests.cs | 80 +++++++++ .../WebApiEditors/ContentControllerTests.cs | 14 -- .../TestHelpers/FakeHttpContextFactory.cs | 5 + src/Umbraco.Tests/Umbraco.Tests.csproj | 5 +- src/Umbraco.Tests/packages.config | 2 + .../src/common/resources/content.resource.js | 2 +- .../Editors/AuthenticationController.cs | 1 + src/Umbraco.Web/Editors/ContentController.cs | 46 ++++-- .../Editors/ContentControllerBase.cs | 16 ++ src/Umbraco.Web/Editors/LegacyController.cs | 1 + src/Umbraco.Web/Editors/MediaController.cs | 24 +-- .../Models/ContentEditing/ContentItemBasic.cs | 79 +++++---- src/Umbraco.Web/Models/PagedResult.cs | 4 + src/Umbraco.Web/Umbraco.Web.csproj | 8 +- .../ContentItemValidationFilterAttribute.cs | 49 ------ ...EnsureUserPermissionForContentAttribute.cs | 81 ++++++++++ .../FileUploadCleanupFilterAttribute.cs | 2 +- .../FilterAllowedOutgoingContentAttribute.cs | 152 ++++++++++++++++++ .../Filters/HttpQueryStringFilterAttribute.cs | 2 +- ....cs => OutgoingDateTimeFormatAttribute.cs} | 2 +- .../UmbracoApplicationAuthorizeAttribute.cs | 34 ++++ .../ValidationFilterAttribute.cs | 52 +++--- .../WebApi/UmbracoAuthorizeAttribute.cs | 11 +- .../WebApi/UmbracoAuthorizedApiController.cs | 1 + src/umbraco.businesslogic/User.cs | 40 +++-- 26 files changed, 533 insertions(+), 182 deletions(-) create mode 100644 src/Umbraco.Tests/Controllers/WebApiEditors/ContentControllerHostedTests.cs delete mode 100644 src/Umbraco.Tests/Controllers/WebApiEditors/ContentControllerTests.cs delete mode 100644 src/Umbraco.Web/WebApi/Filters/ContentItemValidationFilterAttribute.cs create mode 100644 src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs create mode 100644 src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs rename src/Umbraco.Web/WebApi/Filters/{OutgoingDateTimeFormat.cs => OutgoingDateTimeFormatAttribute.cs} (93%) create mode 100644 src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs rename src/Umbraco.Web/WebApi/{ => Filters}/ValidationFilterAttribute.cs (76%) diff --git a/src/Umbraco.Core/TypeHelper.cs b/src/Umbraco.Core/TypeHelper.cs index 26dd2a8402..731a8de1f4 100644 --- a/src/Umbraco.Core/TypeHelper.cs +++ b/src/Umbraco.Core/TypeHelper.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core private static readonly ConcurrentDictionary GetFieldsCache = new ConcurrentDictionary(); private static readonly ConcurrentDictionary, PropertyInfo[]> GetPropertiesCache = new ConcurrentDictionary, PropertyInfo[]>(); - + /// /// Find all assembly references that are referencing the assignTypeFrom Type's assembly found in the assemblyList /// diff --git a/src/Umbraco.Tests/Controllers/WebApiEditors/ContentControllerHostedTests.cs b/src/Umbraco.Tests/Controllers/WebApiEditors/ContentControllerHostedTests.cs new file mode 100644 index 0000000000..cedeeae0aa --- /dev/null +++ b/src/Umbraco.Tests/Controllers/WebApiEditors/ContentControllerHostedTests.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.SelfHost; +using NUnit.Framework; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using umbraco.presentation.channels.businesslogic; + +namespace Umbraco.Tests.Controllers.WebApiEditors +{ + //we REALLY need a way to nicely mock the service context, etc... so we don't have to do integration tests... coming soon. + + //NOTE: The below self hosted stuff does work so need to get some tests written. Some are not possible atm because + // of the legacy SQL calls like checking permissions. + + //[TestFixture] + //public class ContentControllerHostedTests : BaseRoutingTest + //{ + + // protected override DatabaseBehavior DatabaseTestBehavior + // { + // get { return DatabaseBehavior.NoDatabasePerFixture; } + // } + + // public override void TearDown() + // { + // base.TearDown(); + // UmbracoAuthorizeAttribute.Enable = true; + // UmbracoApplicationAuthorizeAttribute.Enable = true; + // } + + // /// + // /// Tests to ensure that the response filter works so that any items the user + // /// doesn't have access to are removed + // /// + // [Test] + // public async void Get_By_Ids_Response_Filtered() + // { + // UmbracoAuthorizeAttribute.Enable = false; + // UmbracoApplicationAuthorizeAttribute.Enable = false; + + // var baseUrl = string.Format("http://{0}:9876", Environment.MachineName); + // var url = baseUrl + "/api/Content/GetByIds?ids=1&ids=2"; + + // var routingCtx = GetRoutingContext(url, 1234, null, true); + + // var config = new HttpSelfHostConfiguration(baseUrl); + // using (var server = new HttpSelfHostServer(config)) + // { + // var route = config.Routes.MapHttpRoute("test", "api/Content/GetByIds", + // new + // { + // controller = "Content", + // action = "GetByIds", + // id = RouteParameter.Optional + // }); + // route.DataTokens["Namespaces"] = new string[] { "Umbraco.Web.Editors" }; + + // var client = new HttpClient(server); + + // var request = new HttpRequestMessage + // { + // RequestUri = new Uri(url), + // Method = HttpMethod.Get + // }; + + // var result = await client.SendAsync(request); + // } + + // } + + //} +} diff --git a/src/Umbraco.Tests/Controllers/WebApiEditors/ContentControllerTests.cs b/src/Umbraco.Tests/Controllers/WebApiEditors/ContentControllerTests.cs deleted file mode 100644 index 49e094edad..0000000000 --- a/src/Umbraco.Tests/Controllers/WebApiEditors/ContentControllerTests.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Tests.Controllers.WebApiEditors -{ - //we REALLY need a way to nicely mock the service context, etc... so we don't have to do integration tests... coming soon. - - //public class ContentControllerTests - //{ - //} -} diff --git a/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs b/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs index 05ba17839c..97ad70f6cf 100644 --- a/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs +++ b/src/Umbraco.Tests/TestHelpers/FakeHttpContextFactory.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Security; +using System.Security.Principal; using System.Text; using System.Web; using System.Web.Routing; @@ -87,6 +88,9 @@ namespace Umbraco.Tests.TestHelpers var server = MockRepository.GenerateStub(); server.Stub(x => x.MapPath(Arg.Is.Anything)).Return(Environment.CurrentDirectory); + //User + var user = MockRepository.GenerateStub(); + //HTTP Context HttpContext = MockRepository.GenerateMock(); @@ -95,6 +99,7 @@ namespace Umbraco.Tests.TestHelpers HttpContext.Stub(x => x.Request).Return(request); HttpContext.Stub(x => x.Server).Return(server); HttpContext.Stub(x => x.Response).Return(response); + HttpContext.Stub(x => x.User).Return(user); RequestContext.Stub(x => x.HttpContext).Return(HttpContext); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 6b7006367f..3136fe0119 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -117,6 +117,9 @@ ..\packages\Microsoft.AspNet.WebApi.Core.4.0.30506.0\lib\net40\System.Web.Http.dll + + ..\packages\Microsoft.AspNet.WebApi.SelfHost.4.0.30506.0\lib\net40\System.Web.Http.SelfHost.dll + ..\packages\Microsoft.AspNet.WebApi.WebHost.4.0.30506.0\lib\net40\System.Web.Http.WebHost.dll @@ -195,7 +198,7 @@ - + diff --git a/src/Umbraco.Tests/packages.config b/src/Umbraco.Tests/packages.config index 13dbc067bd..bfa801a67a 100644 --- a/src/Umbraco.Tests/packages.config +++ b/src/Umbraco.Tests/packages.config @@ -1,5 +1,6 @@  + @@ -9,6 +10,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index bb24d8b617..3931deef32 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -114,7 +114,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { if (options.orderDirection === "asc") { options.orderDirection = "Ascending"; } - else { + else if (options.orderDirection === "desc") { options.orderDirection = "Descending"; } diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 457391dc42..4270a9082d 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -8,6 +8,7 @@ using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; using Umbraco.Web.Security; using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors { diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index feeffa90d4..651ebc6094 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -29,12 +29,16 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Editors { - //TODO: For each of these requests the user will need to have access to the content app! - + /// /// The API controller used for editing content /// + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the content application. + /// [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] public class ContentController : ContentControllerBase { /// @@ -54,12 +58,15 @@ namespace Umbraco.Web.Editors { } + /// + /// Return content for the specified ids + /// + /// + /// + [FilterAllowedOutgoingContent] public IEnumerable GetByIds([FromUri]int[] ids) { var foundContent = ((ContentService) Services.ContentService).GetByIds(ids); - - //TODO: We need to check if the current user is allowed to see each node! - return foundContent.Select(Mapper.Map); } @@ -68,6 +75,7 @@ namespace Umbraco.Web.Editors /// /// /// + [EnsureUserPermissionForContent("id")] public ContentItemDisplay GetById(int id) { var foundContent = Services.ContentService.GetById(id); @@ -75,9 +83,7 @@ namespace Umbraco.Web.Editors { HandleContentNotFound(id); } - - //TODO: We need to check if the current user is allowed to see this node! - + return Mapper.Map(foundContent); } @@ -104,6 +110,7 @@ namespace Umbraco.Web.Editors /// /// [OutgoingDateTimeFormat] + [FilterAllowedOutgoingContent("Items")] public PagedResult> GetChildren( int id, int pageNumber = 0, @@ -117,8 +124,6 @@ namespace Umbraco.Web.Editors //TODO: This will be horribly inefficient for paging! This is because our datasource/repository // doesn't support paging at the SQL level... and it'll be pretty interesting to try to make that work. - //TODO: We need to check the nodes returned to see if the current user is allowed to see each of them! - var foundContent = Services.ContentService.GetById(id); if (foundContent == null) { @@ -250,16 +255,21 @@ namespace Umbraco.Web.Editors /// /// /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Delete access to this node. + /// + [EnsureUserPermissionForContent("id", 'D')] public HttpResponseMessage DeleteById(int id) { + //TODO: We need to check if the user is allowed to do this! + var foundContent = Services.ContentService.GetById(id); if (foundContent == null) { return HandleContentNotFound(id, false); } - //TODO: We need to check if the user is allowed to do this! - //if the current item is in the recycle bin if (foundContent.IsInRecycleBin() == false) { @@ -277,7 +287,11 @@ namespace Umbraco.Web.Editors /// Empties the recycle bin /// /// + /// + /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin + /// [HttpDelete] + [EnsureUserPermissionForContent(true)] public HttpResponseMessage EmptyRecycleBin() { //TODO: We need to check if the user is allowed access to the recycle bin! @@ -293,6 +307,8 @@ namespace Umbraco.Web.Editors /// public HttpResponseMessage PostSort(ContentSortOrder sorted) { + //TODO: We need to check if the user is allowed to sort here! + if (sorted == null) { return Request.CreateResponse(HttpStatusCode.NotFound); @@ -307,15 +323,13 @@ namespace Umbraco.Web.Editors if (Security.UserHasAppAccess(Constants.Applications.Content, UmbracoUser) == false) { return Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "User has no access to this application"); - } - - //TODO: We need to check if the user is allowed to sort here! + } var contentService = Services.ContentService; var sortedContent = new List(); try { - sortedContent.AddRange(sorted.IdSortOrder.Select(contentService.GetById)); + sortedContent.AddRange(((ContentService) Services.ContentService).GetByIds(sorted.IdSortOrder)); // Save content with new sort order and update content xml in db accordingly if (contentService.Sort(sortedContent) == false) diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index bb375ce3b7..0427cd8412 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -56,6 +56,22 @@ namespace Umbraco.Web.Editors } } + protected HttpResponseMessage PerformSort(ContentSortOrder sorted) + { + if (sorted == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //if there's nothing to sort just return ok + if (sorted.IdSortOrder.Length == 0) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + return null; + } + protected void MapPropertyValues(ContentItemSave contentItem) where TPersisted : IContentBase { diff --git a/src/Umbraco.Web/Editors/LegacyController.cs b/src/Umbraco.Web/Editors/LegacyController.cs index 0d840a8f81..e80026001e 100644 --- a/src/Umbraco.Web/Editors/LegacyController.cs +++ b/src/Umbraco.Web/Editors/LegacyController.cs @@ -6,6 +6,7 @@ using Umbraco.Core; using Umbraco.Web.Mvc; using Umbraco.Web.UI; using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors { diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 3226083bcb..0aabae770a 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -22,18 +22,12 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Editors { - //internal interface IUmbracoApiService - //{ - // T Get(int id); - // IEnumerable GetChildren(int id); - // HttpResponseMessage Delete(int id); - // //copy - // //move - // //update - // //create - //} - + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the media application. + /// [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Media)] public class MediaController : ContentControllerBase { /// @@ -76,6 +70,7 @@ namespace Umbraco.Web.Editors /// /// /// + [EnsureUserPermissionForContent("id")] public MediaItemDisplay GetById(int id) { var foundContent = Services.MediaService.GetById(id); @@ -180,12 +175,7 @@ namespace Umbraco.Web.Editors { return Request.CreateResponse(HttpStatusCode.OK); } - - if (Security.UserHasAppAccess(Constants.Applications.Media, UmbracoUser) == false) - { - return Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "User has no access to this application"); - } - + var mediaService = base.ApplicationContext.Services.MediaService; var sortedMedia = new List(); try diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs index 38cc08cf85..fed6b9b959 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs @@ -13,18 +13,8 @@ namespace Umbraco.Web.Models.ContentEditing /// A model representing a basic content item /// [DataContract(Name = "content", Namespace = "")] - public class ContentItemBasic - where T: ContentPropertyBasic - where TPersisted : IContentBase + public class ContentItemBasic { - public ContentItemBasic() - { - //ensure its not null - _properties = new List(); - } - - private IEnumerable _properties; - [DataMember(Name = "icon")] public string Icon { get; set; } @@ -36,13 +26,6 @@ namespace Umbraco.Web.Models.ContentEditing [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] public string Name { get; set; } - [DataMember(Name = "properties")] - public virtual IEnumerable Properties - { - get { return _properties; } - set { _properties = value; } - } - [DataMember(Name = "updateDate")] public DateTime UpdateDate { get; set; } @@ -55,7 +38,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "owner")] public UserBasic Owner { get; set; } - + [DataMember(Name = "updator")] public UserBasic Updator { get; set; } @@ -66,6 +49,48 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "sortOrder")] public int SortOrder { get; set; } + protected bool Equals(ContentItemBasic other) + { + return Id == other.Id; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + var other = obj as ContentItemBasic; + return other != null && Equals(other); + } + + public override int GetHashCode() + { + return Id; + } + } + + /// + /// A model representing a basic content item with properties + /// + [DataContract(Name = "content", Namespace = "")] + public class ContentItemBasic : ContentItemBasic + where T : ContentPropertyBasic + where TPersisted : IContentBase + { + public ContentItemBasic() + { + //ensure its not null + _properties = new List(); + } + + private IEnumerable _properties; + + [DataMember(Name = "properties")] + public virtual IEnumerable Properties + { + get { return _properties; } + set { _properties = value; } + } + /// /// The real persisted content object /// @@ -82,22 +107,6 @@ namespace Umbraco.Web.Models.ContentEditing [JsonIgnore] internal ContentItemDto ContentDto { get; set; } - protected bool Equals(ContentItemBasic other) - { - return Id == other.Id; - } - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - var other = obj as ContentItemBasic; - return other != null && Equals(other); - } - - public override int GetHashCode() - { - return Id; - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/PagedResult.cs b/src/Umbraco.Web/Models/PagedResult.cs index d6545923cc..6d8b7838db 100644 --- a/src/Umbraco.Web/Models/PagedResult.cs +++ b/src/Umbraco.Web/Models/PagedResult.cs @@ -29,12 +29,16 @@ namespace Umbraco.Web.Models [DataMember(Name = "pageNumber")] public long PageNumber { get; private set; } + [DataMember(Name = "pageSize")] public long PageSize { get; private set; } + [DataMember(Name = "totalPages")] public long TotalPages { get; private set; } + [DataMember(Name = "totalItems")] public long TotalItems { get; private set; } + [DataMember(Name = "items")] public IEnumerable Items { get; set; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8ff5738332..ab9def1ba5 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -494,11 +494,13 @@ - + + - + + @@ -754,7 +756,7 @@ - + diff --git a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationFilterAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationFilterAttribute.cs deleted file mode 100644 index 4aaf621e05..0000000000 --- a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationFilterAttribute.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using System.Web.UI; -using Umbraco.Core.PropertyEditors; -using Umbraco.Web.Models.Mapping; - -namespace Umbraco.Web.WebApi.Filters -{ - ///// - ///// Validates the content item - ///// - ///// - ///// There's various validation happening here both value validation and structure validation - ///// to ensure that malicious folks are not trying to post invalid values or to invalid properties. - ///// - //internal class ContentItemValidationFilterAttribute : ActionFilterAttribute - //{ - // private readonly Type _helperType; - // private readonly dynamic _helper; - - // public ContentItemValidationFilterAttribute(Type helperType) - // { - // _helperType = helperType; - // _helper = Activator.CreateInstance(helperType); - // } - - // /// - // /// Returns true so that other filters can execute along with this one - // /// - // public override bool AllowMultiple - // { - // get { return true; } - // } - - // /// - // /// Performs the validation - // /// - // /// - // public override void OnActionExecuting(HttpActionContext actionContext) - // { - // _helper.ValidateItem(actionContext); - // } - - - - //} -} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs new file mode 100644 index 0000000000..97386052b7 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections; +using System.Linq; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using Umbraco.Core.Models; +using umbraco.BusinessLogic.Actions; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Auth filter to check if the current user has access to the content item (by id). + /// + /// + /// + /// This first checks if the user can access this based on their start node, and then checks node permissions + /// TODO: Implement start node check!!! + /// + /// By default the permission that is checked is browse but this can be specified in the ctor. + /// NOTE: This cannot be an auth filter because that happens too soon and we don't have access to the action params. + /// + internal sealed class EnsureUserPermissionForContentAttribute : ActionFilterAttribute + { + private readonly bool _onlyCheckStartNode; + private readonly string _paramName; + private readonly char _permissionToCheck; + + public EnsureUserPermissionForContentAttribute(bool onlyCheckStartNode) + { + _onlyCheckStartNode = onlyCheckStartNode; + } + public EnsureUserPermissionForContentAttribute(string paramName) + { + _paramName = paramName; + _permissionToCheck = ActionBrowse.Instance.Letter; + } + public EnsureUserPermissionForContentAttribute(string paramName, char permissionToCheck) + { + _paramName = paramName; + _permissionToCheck = permissionToCheck; + } + + public override bool AllowMultiple + { + get { return true; } + } + + public override void OnActionExecuting(HttpActionContext actionContext) + { + if (_onlyCheckStartNode) + { + //TODO: implement this as well! + } + + if (UmbracoContext.Current.UmbracoUser == null) + { + throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); + } + + if (actionContext.ActionArguments[_paramName] == null) + { + throw new InvalidOperationException("No argument found for the current action with the name: " + _paramName); + } + + var nodeId = (int)actionContext.ActionArguments[_paramName]; + + //TODO: Change these calls to a service layer call and make sure we can mock it! + var permissions = UmbracoContext.Current.UmbracoUser.GetPermissions(nodeId); + if (permissions.ToCharArray().Contains(_permissionToCheck)) + { + base.OnActionExecuting(actionContext); + } + else + { + throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); + } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs index 91d42fa123..28f5a831f5 100644 --- a/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FileUploadCleanupFilterAttribute.cs @@ -9,7 +9,7 @@ namespace Umbraco.Web.WebApi.Filters /// /// Checks if the parameter is ContentItemSave and then deletes any temporary saved files from file uploads associated with the request /// - internal class FileUploadCleanupFilterAttribute : ActionFilterAttribute + internal sealed class FileUploadCleanupFilterAttribute : ActionFilterAttribute { /// /// Returns true so that other filters can execute along with this one diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs new file mode 100644 index 0000000000..c2e270e6ab --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Web.Http.Filters; +using Umbraco.Web.Models.ContentEditing; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core; + +namespace Umbraco.Web.WebApi.Filters +{ + + //TODO: Verify that this works!! + + /// + /// This inspects the result of the action that returns a collection of content and removes + /// any item that the current user doesn't have access to + /// + internal sealed class FilterAllowedOutgoingContentAttribute : ActionFilterAttribute + { + private readonly string _propertyName; + private readonly char _permissionToCheck; + + public FilterAllowedOutgoingContentAttribute() + { + _permissionToCheck = ActionBrowse.Instance.Letter; + } + + public FilterAllowedOutgoingContentAttribute(char permissionToCheck) + { + _permissionToCheck = permissionToCheck; + } + + public FilterAllowedOutgoingContentAttribute(string propertyName) + : this() + { + _propertyName = propertyName; + } + + /// + /// Returns true so that other filters can execute along with this one + /// + public override bool AllowMultiple + { + get { return true; } + } + + public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) + { + var user = UmbracoContext.Current.UmbracoUser; + if (user == null) + { + base.OnActionExecuted(actionExecutedContext); + return; + } + + var objectContent = actionExecutedContext.Response.Content as ObjectContent; + if (objectContent != null) + { + var collection = GetValueFromResponse(objectContent); + + if (collection != null) + { + var items = Enumerable.ToList(collection); + var length = items.Count; + for (var i = 0; i < length; i++) + { + var permissions = user.GetPermissions(items[i].Id); + if (Enumerable.Contains(permissions.ToCharArray(), _permissionToCheck) == false) + { + items.RemoveAt(i); + length--; + } + } + + //set the return value + SetValueForResponse(objectContent, items); + } + } + + base.OnActionExecuted(actionExecutedContext); + } + + private void SetValueForResponse(ObjectContent objectContent, dynamic newVal) + { + var t = objectContent.Value.GetType(); + + if (objectContent.Value is IEnumerable) + { + //objectContent.Value = DynamicCast(newVal, t); + objectContent.Value = newVal; + } + else if (_propertyName.IsNullOrWhiteSpace() == false) + { + //try to get the enumerable collection from a property on the result object using reflection + var property = objectContent.Value.GetType().GetProperty(_propertyName); + if (property != null) + { + //property.SetValue(objectContent.Value, DynamicCast(newVal, property.PropertyType)); + property.SetValue(objectContent.Value, newVal); + } + } + + } + + private dynamic GetValueFromResponse(ObjectContent objectContent) + { + if (objectContent.Value is IEnumerable) + { + return objectContent.Value; + } + + if (_propertyName.IsNullOrWhiteSpace() == false) + { + //try to get the enumerable collection from a property on the result object using reflection + var property = objectContent.Value.GetType().GetProperty(_propertyName); + if (property != null) + { + var result = property.GetValue(objectContent.Value); + if (result is IEnumerable) + { + return result; + } + } + } + + return null; + } + + //private object DynamicCast(object val, Type outgoingType) + //{ + // var castMethod = GetType() + // .GetMethod("Cast", BindingFlags.Static | BindingFlags.Public) + // .MakeGenericMethod(outgoingType); + + // var castedObject = castMethod.Invoke(null, new object[] { val }); + // return castedObject; + //} + + ///// + ///// Used for dynamic casting + ///// + ///// + ///// + ///// + //public static T Cast(object o) + //{ + // return (T)o; + //} + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/HttpQueryStringFilterAttribute.cs b/src/Umbraco.Web/WebApi/Filters/HttpQueryStringFilterAttribute.cs index c770003310..5844a510f0 100644 --- a/src/Umbraco.Web/WebApi/Filters/HttpQueryStringFilterAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/HttpQueryStringFilterAttribute.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.WebApi.Filters /// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number /// but this will allow you to do it /// - public class HttpQueryStringFilterAttribute : ActionFilterAttribute + public sealed class HttpQueryStringFilterAttribute : ActionFilterAttribute { public string ParameterName { get; private set; } diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormat.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs similarity index 93% rename from src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormat.cs rename to src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs index 8f8f26205a..dd8e30e34f 100644 --- a/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormat.cs +++ b/src/Umbraco.Web/WebApi/Filters/OutgoingDateTimeFormatAttribute.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.WebApi.Filters /// /// Sets the json outgoing/serialized datetime format /// - internal class OutgoingDateTimeFormatAttribute : ActionFilterAttribute + internal sealed class OutgoingDateTimeFormatAttribute : ActionFilterAttribute { private readonly string _format; diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs new file mode 100644 index 0000000000..77a4fe523e --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoApplicationAuthorizeAttribute.cs @@ -0,0 +1,34 @@ +using System.Web.Http; +using System.Web.Http.Controllers; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Ensures that the current user has access to the specified application + /// + internal sealed class UmbracoApplicationAuthorizeAttribute : AuthorizeAttribute + { + /// + /// Can be used by unit tests to enable/disable this filter + /// + internal static bool Enable = true; + + private readonly string _appName; + + public UmbracoApplicationAuthorizeAttribute(string appName) + { + _appName = appName; + } + + protected override bool IsAuthorized(HttpActionContext actionContext) + { + if (Enable == false) + { + return true; + } + + return UmbracoContext.Current.UmbracoUser != null + && UmbracoContext.Current.Security.UserHasAppAccess(_appName, UmbracoContext.Current.UmbracoUser); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/ValidationFilterAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ValidationFilterAttribute.cs similarity index 76% rename from src/Umbraco.Web/WebApi/ValidationFilterAttribute.cs rename to src/Umbraco.Web/WebApi/Filters/ValidationFilterAttribute.cs index 07cabcbb16..cce0bd06d4 100644 --- a/src/Umbraco.Web/WebApi/ValidationFilterAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/ValidationFilterAttribute.cs @@ -1,28 +1,26 @@ -using System.ComponentModel.DataAnnotations; -using System.Net; -using System.Net.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; - -namespace Umbraco.Web.WebApi -{ - - /// - /// An action filter used to do basic validation against the model and return a result - /// straight away if it fails. - /// - internal class ValidationFilterAttribute : ActionFilterAttribute - { - public override void OnActionExecuting(HttpActionContext actionContext) - { - var modelState = actionContext.ModelState; - - if (!modelState.IsValid) - { - actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, modelState); - } - - } - } - +using System.Net; +using System.Net.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// An action filter used to do basic validation against the model and return a result + /// straight away if it fails. + /// + internal sealed class ValidationFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(HttpActionContext actionContext) + { + var modelState = actionContext.ModelState; + + if (!modelState.IsValid) + { + actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, modelState); + } + + } + } + } \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs index e804158b4a..337892e0ca 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs @@ -1,7 +1,6 @@ using System; using System.Web.Http; using Umbraco.Core; -using Umbraco.Web.Security; namespace Umbraco.Web.WebApi { @@ -10,6 +9,11 @@ namespace Umbraco.Web.WebApi /// public sealed class UmbracoAuthorizeAttribute : AuthorizeAttribute { + /// + /// Can be used by unit tests to enable/disable this filter + /// + internal static bool Enable = true; + private readonly ApplicationContext _applicationContext; private readonly UmbracoContext _umbracoContext; @@ -40,6 +44,11 @@ namespace Umbraco.Web.WebApi protected override bool IsAuthorized(System.Web.Http.Controllers.HttpActionContext actionContext) { + if (Enable == false) + { + return true; + } + try { var appContext = GetApplicationContext(); diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index d03c52e158..956ed6ca88 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -3,6 +3,7 @@ using System.Web; using System.Web.Http; using Umbraco.Core.Configuration; using Umbraco.Web.Security; +using Umbraco.Web.WebApi.Filters; using umbraco.BusinessLogic; namespace Umbraco.Web.WebApi diff --git a/src/umbraco.businesslogic/User.cs b/src/umbraco.businesslogic/User.cs index 5da83d2a6d..8a2af06082 100644 --- a/src/umbraco.businesslogic/User.cs +++ b/src/umbraco.businesslogic/User.cs @@ -666,44 +666,56 @@ namespace umbraco.BusinessLogic { if (!_isInitialized) setupUser(_id); + + // NH 4.7.1 changing default permission behavior to default to User Type permissions IF no specific permissions has been + // set for the current node + var nodeId = Path.Contains(",") ? int.Parse(Path.Substring(Path.LastIndexOf(",", StringComparison.Ordinal)+1)) : int.Parse(Path); + return GetPermissions(nodeId); + } + + [Obsolete("Do not use this, implement something in the service layer!! And make sure we can mock/test it")] + internal string GetPermissions(int nodeId) + { + if (!_isInitialized) + setupUser(_id); + string defaultPermissions = UserType.DefaultPermissions; //get the cached permissions for the user var cachedPermissions = ApplicationContext.Current.ApplicationCache.GetCacheItem( string.Format("{0}{1}", CacheKeys.UserPermissionsCacheKey, _id), //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will make this priority below average - CacheItemPriority.BelowNormal, + CacheItemPriority.BelowNormal, null, //Since this cache can be quite large (http://issues.umbraco.org/issue/U4-2161) we will only have this exist in cache for 20 minutes, // then it will refresh from the database. new TimeSpan(0, 20, 0), () => + { + var cruds = new Hashtable(); + using (var dr = SqlHelper.ExecuteReader("select * from umbracoUser2NodePermission where userId = @userId order by nodeId", SqlHelper.CreateParameter("@userId", this.Id))) { - var cruds = new Hashtable(); - using (var dr = SqlHelper.ExecuteReader("select * from umbracoUser2NodePermission where userId = @userId order by nodeId", SqlHelper.CreateParameter("@userId", this.Id))) + while (dr.Read()) { - while (dr.Read()) + if (!cruds.ContainsKey(dr.GetInt("nodeId"))) { - if (!cruds.ContainsKey(dr.GetInt("nodeId"))) - { - cruds.Add(dr.GetInt("nodeId"), string.Empty); - } - cruds[dr.GetInt("nodeId")] += dr.GetString("permission"); + cruds.Add(dr.GetInt("nodeId"), string.Empty); } + cruds[dr.GetInt("nodeId")] += dr.GetString("permission"); } - return cruds; - }); + } + return cruds; + }); // NH 4.7.1 changing default permission behavior to default to User Type permissions IF no specific permissions has been // set for the current node - var nodeId = Path.Contains(",") ? int.Parse(Path.Substring(Path.LastIndexOf(",", StringComparison.Ordinal)+1)) : int.Parse(Path); if (cachedPermissions.ContainsKey(nodeId)) { - return cachedPermissions[int.Parse(Path.Substring(Path.LastIndexOf(",", StringComparison.Ordinal) + 1))].ToString(); + return cachedPermissions[nodeId].ToString(); } // exception to everything. If default cruds is empty and we're on root node; allow browse of root node - if (string.IsNullOrEmpty(defaultPermissions) && Path == "-1") + if (string.IsNullOrEmpty(defaultPermissions) && nodeId == -1) defaultPermissions = "F"; // else return default user type cruds