From fda866fc9e63eb7bb2cf661fbca64664fe805018 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Mon, 11 Dec 2023 08:25:29 +0100 Subject: [PATCH] V14: Add authorization policies to Management API controllers - p2 (#15211) * Making ProblemDetails details more generic * Adding authorizer that can be replaces for external authz in handlers. Adding handler and requirement for UserBelongsToUserGroupInRequest policy * Adding method to get the GUID from claims * Adding service methods to check user group authz * Porting MustSatisfyRequirementAuthorizationHandler * Adding controllers authz * Fix return status code + produced response type * Moving to folder * Adding DenyLocalLogin policy scaffold * Implement a temp DenyLocalLoginHandler * Introducing a new Fobidden result * Fix comment * Introducing a helper class for authorizers * Changed nullability for GetCurrentUser * Changes from Attempt to Status + FIXME comments * Create a UserGroupAuthorizationStatus to be used in the future * Introduces a new authz status for checking media acess * Introducing a new permission service for media * Adding fixme * Adding more policy configurations * Adding Media policy requirement and handler * Adding media authorizer * Fix order of params * Adding duplicate code comment * Adding authz to media controllers * Migrating more logic from MediaPermissions.cs * Adding more MediaAuthorizationStatus-es * Handling of new authorization status * Fix comment * Adding NotFound case * Adding NewDenyLocalLoginIfConfigured policy && commenting [AllowAnonymous] where the policy is applied since it is already handled * Changed Forbid() to Forbidden() to get the correct status code * Remove policy that is applied on the base controller already * Implement and apply NewUmbracoFeatureEnabled policy * Renaming classes to add Permission in the name * Register permission services * Add FIXME * Introduce new IUserGroupPermissionService and refactor accordingly * Add single overload with default implementation * Adding user permission policy and related * Applying admin policy * Register all new policies * Better wording * Add default implementation for a single overload * Adding remarks to IContentPermissionService.cs * Supporting null as key in ContentPermissionService * Fix namespace * Reverting back to not supporting null as content key, but having dedicated implementation * Adding content authorizer with null values to represent root item * Removing null key support and adding dedicated implementation * Removing remarks * Adding content resource with null support * Removing null support * Adding requirement and status * Adding content authorizer + handlers * Applying policies to content controllers * Update comment * Handling of Authorization Statuses * More authz in controllers * Fix comments * New branch handler * Obsolete old implementation * Adding dedicated policies to root and bin * Adding a branch specific namespace * Bin specific requirement and namespace * Root specific requirement and namespace * Changing to new root policy * Refactoring * Save policies * Fix null check/reference * Add TODO comment * Create media root- and bin-specific policies, handlers, etc. * Apply correct policy in create and update media controllers * Apply root policy to move and sort controllers * Fix wording * Adding UserGroupAuthorizationStatusResult * Remove all AuthorizationStatusResult as we cannot get the specific AuthorizationStatus * Fixing Umbraco feature policy * Fix allow anonymous endpoints - the value returned from DenyLocalLoginHandler wasn't enough, we need to succeed DenyAnonymousAuthorizationRequirement as it is required for some of the endpoints that had the attribute * Apply DenyLocalLoginIfConfigured policy to corresponding re-implementation of PostSetInvitedUserPassword * Fix comment * Renaming performingUser to user and fixing comments * Rename helper method * Fix references * Re-add merge conflict deletion * Adding Backoffice requirement and relevant * Registering * Added a simple policy test * Fixed small test things and clean up * Temp solution * Added one more test and fix another static issue * Fix another merge conflict * Remove BackOfficePermissionRequirement and handler as they might not be necessary * Comment out again [AllowAnonymous] * Remove AuthorizationPolicies.BackOfficeAccessWithoutApproval policy as it might not be necessary * Fix temp implementation * Fix reference to correct handler * Apply authz policy to new publish/unpublish controllers * Fix comments * Removing duplicate ProducesResponseTypes * Added swagger documentation about the 401 and 403 * Added Resources to Media, User and UserGroup * Handle root, recycle bin and branch in the same handler * Handle both parent and target when moving * Check Ids for all sort requests * Xml docs * Clean up * Clean up * Fix build * Cleanup * Remove TODO * Added missing overload * Use yield * Adding some keys to check --------- Co-authored-by: Bjarke Berg Co-authored-by: Andreas Zerbst --- .../UmbracoBuilderAuthExtensions.cs | 5 +- .../Controllers/DeliveryApiControllerBase.cs | 6 +- .../Document/ByKeyDocumentController.cs | 22 +- .../Document/CopyDocumentController.cs | 23 +- .../Document/CreateDocumentController.cs | 28 +- .../CreatePublicAccessDocumentController.cs | 27 +- .../DeletePublicAccessDocumentController.cs | 22 +- .../GetPublicAccessDocumentController.cs | 18 + .../Document/MoveDocumentController.cs | 23 +- .../MoveToRecycleBinDocumentController.cs | 22 +- .../Document/NotificationsController.cs | 22 +- .../Document/PublishDocumentController.cs | 22 +- ...ublishDocumentWithDescendantsController.cs | 22 +- .../DocumentRecycleBinControllerBase.cs | 1 - .../Document/SortDocumentController.cs | 22 +- .../Document/UnpublishDocumentController.cs | 23 +- .../Document/UpdateDocumentController.cs | 19 +- .../UpdatePublicAccessDocumentController.cs | 19 +- .../ManagementApiControllerBase.cs | 15 +- .../Controllers/Media/ByKeyMediaController.cs | 21 +- .../Media/CreateMediaController.cs | 28 +- .../Controllers/Media/MoveMediaController.cs | 22 +- .../Media/MoveToRecycleBinMediaController.cs | 21 +- .../MediaRecycleBinControllerBase.cs | 1 - .../Controllers/Media/SortMediaController.cs | 20 +- .../Media/UpdateMediaController.cs | 18 +- .../Security/BackOfficeController.cs | 5 +- .../Security/ResetPasswordController.cs | 4 +- .../Security/ResetPasswordTokenController.cs | 3 +- .../Security/SecurityControllerBase.cs | 3 + .../VerifyResetPasswordTokenController.cs | 3 +- .../User/BulkDeleteUsersController.cs | 21 +- .../Controllers/User/ByKeyUsersController.cs | 17 + .../User/ClearAvatarUsersController.cs | 21 +- .../CreateInitialPasswordUserController.cs | 4 +- .../User/Current/GetCurrentUserController.cs | 17 + .../Current/SetAvatarCurrentUserController.cs | 25 +- .../Controllers/User/DeleteUsersController.cs | 27 +- .../User/DisableUsersController.cs | 21 +- .../Controllers/User/EnableUsersController.cs | 21 +- .../User/SetAvatarUsersController.cs | 18 +- .../Controllers/User/UnlockUsersController.cs | 21 +- .../User/UpdateUserGroupsUsersController.cs | 18 +- .../Controllers/User/UsersControllerBase.cs | 2 +- .../User/VerifyInviteUsersController.cs | 4 +- .../BulkDeleteUserGroupsController.cs | 18 +- .../UserGroup/ByKeyUserGroupController.cs | 17 + .../UserGroup/DeleteUserGroupController.cs | 18 +- .../UserGroup/UserGroupsControllerBase.cs | 2 +- .../BackOfficeAuthPolicyBuilderExtensions.cs | 70 +- .../UmbracoBuilderExtensions.cs | 88 ++ .../ManagementApiComposer.cs | 79 +- ...ceAuthorizationInitializationMiddleware.cs | 4 +- src/Umbraco.Cms.Api.Management/OpenApi.json | 916 +++++++++++++++++- ...ficeSecurityRequirementsOperationFilter.cs | 26 +- .../Authorization/AuthorizationHelper.cs | 48 + .../AuthorizationServiceExtensions.cs | 11 + .../Content/ContentPermissionAuthorizer.cs | 65 ++ .../Content/ContentPermissionHandler.cs | 49 + .../Content/ContentPermissionRequirement.cs | 10 + .../Content/ContentPermissionResource.cs | 147 +++ .../Content/IContentPermissionAuthorizer.cs | 82 ++ .../DenyLocalLogin/DenyLocalLoginHandler.cs | 45 + .../DenyLocalLoginRequirement.cs | 10 + .../Feature/FeatureAuthorizeHandler.cs | 22 + .../Feature/FeatureAuthorizeRequirement.cs | 10 + .../Feature/FeatureAuthorizer.cs | 67 ++ .../Feature/IFeatureAuthorizer.cs | 16 + .../Authorization/IAuthorizationHelper.cs | 19 + .../Authorization/IPermissionResource.cs | 5 + .../Media/IMediaPermissionAuthorizer.cs | 41 + .../Media/MediaPermissionAuthorizer.cs | 55 ++ .../Media/MediaPermissionHandler.cs | 44 + .../Media/MediaPermissionRequirement.cs | 10 + .../Media/MediaPermissionResource.cs | 82 ++ ...tSatisfyRequirementAuthorizationHandler.cs | 78 ++ .../User/IUserPermissionAuthorizer.cs | 27 + .../User/UserPermissionAuthorizer.cs | 35 + .../User/UserPermissionHandler.cs | 25 + .../User/UserPermissionRequirement.cs | 10 + .../User/UserPermissionResource.cs | 34 + .../IUserGroupPermissionAuthorizer.cs | 27 + .../UserGroupPermissionAuthorizer.cs | 35 + .../UserGroup/UserGroupPermissionHandler.cs | 25 + .../UserGroupPermissionRequirement.cs | 10 + .../UserGroup/UserGroupPermissionResource.cs | 34 + .../Security/BackOfficeApplicationManager.cs | 39 +- .../DependencyInjection/UmbracoBuilder.cs | 3 + .../Extensions/ClaimsIdentityExtensions.cs | 25 + .../Security/ContentPermissions.cs | 11 +- src/Umbraco.Core/Security/MediaPermissions.cs | 1 + src/Umbraco.Core/Services/AuditService.cs | 6 +- .../ContentAuthorizationStatus.cs | 11 + .../MediaAuthorizationStatus.cs | 10 + .../UserAuthorizationStatus.cs | 7 + .../Services/ContentPermissionService.cs | 158 +++ .../Services/IContentPermissionService.cs | 83 ++ .../Services/IMediaPermissionService.cs | 42 + .../Services/IUserGroupPermissionService.cs | 3 +- .../Services/IUserGroupService.cs | 7 +- .../Services/IUserPermissionService.cs | 28 + .../Services/MediaPermissionService.cs | 56 ++ .../UserGroupOperationStatus.cs | 1 + .../Services/UserPermissionService.cs | 31 + .../Authorization/BackOfficeRequirement.cs | 2 +- .../ManagementApi/ManagementApiTest.cs | 143 +++ .../Policies/ByKeyAuditLogControllerTests.cs | 46 + .../UmbracoTestServerTestBase.cs | 84 +- .../UmbracoWebApplicationFactory.cs | 2 + 109 files changed, 3750 insertions(+), 212 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/AuthorizationHelper.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/AuthorizationServiceExtensions.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionAuthorizer.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionHandler.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionRequirement.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionResource.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Content/IContentPermissionAuthorizer.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginRequirement.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizeHandler.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizeRequirement.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizer.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/IFeatureAuthorizer.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/IAuthorizationHelper.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/IPermissionResource.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Media/IMediaPermissionAuthorizer.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionAuthorizer.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionHandler.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionRequirement.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionResource.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/MustSatisfyRequirementAuthorizationHandler.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/User/IUserPermissionAuthorizer.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionAuthorizer.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionHandler.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionRequirement.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionResource.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/IUserGroupPermissionAuthorizer.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionAuthorizer.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionHandler.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionRequirement.cs create mode 100644 src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionResource.cs create mode 100644 src/Umbraco.Core/Services/AuthorizationStatus/ContentAuthorizationStatus.cs create mode 100644 src/Umbraco.Core/Services/AuthorizationStatus/MediaAuthorizationStatus.cs create mode 100644 src/Umbraco.Core/Services/AuthorizationStatus/UserAuthorizationStatus.cs create mode 100644 src/Umbraco.Core/Services/ContentPermissionService.cs create mode 100644 src/Umbraco.Core/Services/IContentPermissionService.cs create mode 100644 src/Umbraco.Core/Services/IMediaPermissionService.cs create mode 100644 src/Umbraco.Core/Services/IUserPermissionService.cs create mode 100644 src/Umbraco.Core/Services/MediaPermissionService.cs create mode 100644 src/Umbraco.Core/Services/UserPermissionService.cs create mode 100644 tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs create mode 100644 tests/Umbraco.Tests.Integration/ManagementApi/Policies/ByKeyAuditLogControllerTests.cs diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs index 30fec59c23..bf912a5ef9 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs @@ -10,14 +10,11 @@ namespace Umbraco.Cms.Api.Common.DependencyInjection; public static class UmbracoBuilderAuthExtensions { - private static bool _initialized; - public static IUmbracoBuilder AddUmbracoOpenIddict(this IUmbracoBuilder builder) { - if (_initialized is false) + if (builder.Services.Any(x=>x.ImplementationType == typeof(OpenIddictCleanup)) is false) { ConfigureOpenIddict(builder); - _initialized = true; } return builder; diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/DeliveryApiControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/DeliveryApiControllerBase.cs index 0542d8d30c..76d14b8f68 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/DeliveryApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/DeliveryApiControllerBase.cs @@ -1,16 +1,20 @@ using Microsoft.AspNetCore.Mvc; using System.Net; +using Microsoft.AspNetCore.Authorization; using Umbraco.Cms.Api.Common.Attributes; using Umbraco.Cms.Api.Common.Filters; using Umbraco.Cms.Api.Delivery.Configuration; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Features; +using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Delivery.Controllers; [ApiController] [JsonOptionsName(Constants.JsonOptionsNames.DeliveryApi)] [MapToApi(DeliveryApiConfiguration.ApiName)] -public abstract class DeliveryApiControllerBase : Controller +[Authorize(Policy = "New" + AuthorizationPolicies.UmbracoFeatureEnabled)] +public abstract class DeliveryApiControllerBase : Controller, IUmbracoFeature { protected string DecodePath(string path) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs index 711daa19c1..5a488a3cc3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs @@ -1,21 +1,31 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class ByKeyDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IContentEditingService _contentEditingService; private readonly IDocumentPresentationFactory _documentPresentationFactory; - public ByKeyDocumentController(IContentEditingService contentEditingService, IDocumentPresentationFactory documentPresentationFactory) + public ByKeyDocumentController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IDocumentPresentationFactory documentPresentationFactory) { + _authorizationService = authorizationService; _contentEditingService = contentEditingService; _documentPresentationFactory = documentPresentationFactory; } @@ -26,6 +36,16 @@ public class ByKeyDocumentController : DocumentControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task ByKey(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + IContent? content = await _contentEditingService.GetAsync(id); if (content == null) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/CopyDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/CopyDocumentController.cs index 0b7bbd0f90..f331c912e5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/CopyDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/CopyDocumentController.cs @@ -1,23 +1,33 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class CopyDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IContentEditingService _contentEditingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public CopyDocumentController(IContentEditingService contentEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public CopyDocumentController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _contentEditingService = contentEditingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -26,9 +36,18 @@ public class CopyDocumentController : DocumentControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Copy(Guid id, CopyDocumentRequestModel copyDocumentRequestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionCopy.ActionLetter, new[] { copyDocumentRequestModel.TargetId, id }), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt result = await _contentEditingService.CopyAsync( id, copyDocumentRequestModel.TargetId, diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentController.cs index 378c79d9eb..e06c9f9736 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentController.cs @@ -1,28 +1,39 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class CreateDocumentController : DocumentControllerBase { - private readonly IContentEditingService _contentEditingService; + private readonly IAuthorizationService _authorizationService; private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory; + private readonly IContentEditingService _contentEditingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public CreateDocumentController(IContentEditingService contentEditingService, IDocumentEditingPresentationFactory documentEditingPresentationFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public CreateDocumentController( + IAuthorizationService authorizationService, + IDocumentEditingPresentationFactory documentEditingPresentationFactory, + IContentEditingService contentEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { - _contentEditingService = contentEditingService; + _authorizationService = authorizationService; _documentEditingPresentationFactory = documentEditingPresentationFactory; + _contentEditingService = contentEditingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -30,9 +41,18 @@ public class CreateDocumentController : DocumentControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Create(CreateDocumentRequestModel requestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.ParentId), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + ContentCreateModel model = _documentEditingPresentationFactory.MapCreateModel(requestModel); Attempt result = await _contentEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor)); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreatePublicAccessDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreatePublicAccessDocumentController.cs index 76a8757309..f18d93e06b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreatePublicAccessDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreatePublicAccessDocumentController.cs @@ -1,36 +1,53 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.PublicAccess; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class CreatePublicAccessDocumentController : DocumentControllerBase { - private readonly IPublicAccessService _publicAccessService; + private readonly IAuthorizationService _authorizationService; private readonly IPublicAccessPresentationFactory _publicAccessPresentationFactory; + private readonly IPublicAccessService _publicAccessService; public CreatePublicAccessDocumentController( - IPublicAccessService publicAccessService, - IPublicAccessPresentationFactory publicAccessPresentationFactory) + IAuthorizationService authorizationService, + IPublicAccessPresentationFactory publicAccessPresentationFactory, + IPublicAccessService publicAccessService) { - _publicAccessService = publicAccessService; + _authorizationService = authorizationService; _publicAccessPresentationFactory = publicAccessPresentationFactory; + _publicAccessService = publicAccessService; } [MapToApiVersion("1.0")] [HttpPost("{id:guid}/public-access")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Create(Guid id, PublicAccessRequestModel publicAccessRequestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionProtect.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + PublicAccessEntrySlim publicAccessEntrySlim = _publicAccessPresentationFactory.CreatePublicAccessEntrySlim(publicAccessRequestModel, id); Attempt saveAttempt = await _publicAccessService.CreateAsync(publicAccessEntrySlim); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/DeletePublicAccessDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/DeletePublicAccessDocumentController.cs index 08a1daf62b..9d15979dd1 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/DeletePublicAccessDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/DeletePublicAccessDocumentController.cs @@ -1,16 +1,26 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class DeletePublicAccessDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IPublicAccessService _publicAccessService; - public DeletePublicAccessDocumentController(IPublicAccessService publicAccessService) => _publicAccessService = publicAccessService; + public DeletePublicAccessDocumentController(IAuthorizationService authorizationService, IPublicAccessService publicAccessService) + { + _authorizationService = authorizationService; + _publicAccessService = publicAccessService; + } [MapToApiVersion("1.0")] [HttpDelete("{id:guid}/public-access")] @@ -18,6 +28,16 @@ public class DeletePublicAccessDocumentController : DocumentControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Delete(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionProtect.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + await _publicAccessService.DeleteAsync(id); return Ok(); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/GetPublicAccessDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/GetPublicAccessDocumentController.cs index ef830bc716..7f5df688ce 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/GetPublicAccessDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/GetPublicAccessDocumentController.cs @@ -1,25 +1,33 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.PublicAccess; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class GetPublicAccessDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IPublicAccessService _publicAccessService; private readonly IPublicAccessPresentationFactory _publicAccessPresentationFactory; public GetPublicAccessDocumentController( + IAuthorizationService authorizationService, IPublicAccessService publicAccessService, IPublicAccessPresentationFactory publicAccessPresentationFactory) { + _authorizationService = authorizationService; _publicAccessService = publicAccessService; _publicAccessPresentationFactory = publicAccessPresentationFactory; } @@ -29,6 +37,16 @@ public class GetPublicAccessDocumentController : DocumentControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task GetPublicAccess(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionProtect.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt accessAttempt = await _publicAccessService.GetEntryByContentKeyAsync(id); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs index 7cc05fee6e..7ec40f896f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs @@ -1,23 +1,33 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class MoveDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IContentEditingService _contentEditingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public MoveDocumentController(IContentEditingService contentEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public MoveDocumentController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _contentEditingService = contentEditingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -26,9 +36,18 @@ public class MoveDocumentController : DocumentControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Move(Guid id, MoveDocumentRequestModel moveDocumentRequestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionMove.ActionLetter, new[] { moveDocumentRequestModel.TargetId, id }), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt result = await _contentEditingService.MoveAsync( id, moveDocumentRequestModel.TargetId, diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveToRecycleBinDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveToRecycleBinDocumentController.cs index 3b5bead780..3b974f176b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveToRecycleBinDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveToRecycleBinDocumentController.cs @@ -1,22 +1,32 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class MoveToRecycleBinDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IContentEditingService _contentEditingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public MoveToRecycleBinDocumentController(IContentEditingService contentEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public MoveToRecycleBinDocumentController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _contentEditingService = contentEditingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -28,6 +38,16 @@ public class MoveToRecycleBinDocumentController : DocumentControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task MoveToRecycleBin(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionDelete.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt result = await _contentEditingService.MoveToRecycleBinAsync(id, CurrentUserKey(_backOfficeSecurityAccessor)); return result.Success ? Ok() diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/NotificationsController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/NotificationsController.cs index 9a46de5f1c..48e318c50a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/NotificationsController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/NotificationsController.cs @@ -1,21 +1,31 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class NotificationsController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IContentEditingService _contentEditingService; private readonly IDocumentNotificationPresentationFactory _documentNotificationPresentationFactory; - public NotificationsController(IContentEditingService contentEditingService, IDocumentNotificationPresentationFactory documentNotificationPresentationFactory) + public NotificationsController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IDocumentNotificationPresentationFactory documentNotificationPresentationFactory) { + _authorizationService = authorizationService; _contentEditingService = contentEditingService; _documentNotificationPresentationFactory = documentNotificationPresentationFactory; } @@ -26,6 +36,16 @@ public class NotificationsController : DocumentControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Notifications(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + IContent? content = await _contentEditingService.GetAsync(id); return content != null ? Ok(await _documentNotificationPresentationFactory.CreateNotificationModelsAsync(content)) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs index 216874365f..746251e544 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs @@ -1,22 +1,32 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class PublishDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IContentPublishingService _contentPublishingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public PublishDocumentController(IContentPublishingService contentPublishingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public PublishDocumentController( + IAuthorizationService authorizationService, + IContentPublishingService contentPublishingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _contentPublishingService = contentPublishingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -28,6 +38,16 @@ public class PublishDocumentController : DocumentControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Publish(Guid id, PublishDocumentRequestModel requestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt attempt = await _contentPublishingService.PublishAsync( id, requestModel.Cultures, diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs index bd8a1c95ad..98cd287efe 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs @@ -1,22 +1,32 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class PublishDocumentWithDescendantsController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IContentPublishingService _contentPublishingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public PublishDocumentWithDescendantsController(IContentPublishingService contentPublishingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public PublishDocumentWithDescendantsController( + IAuthorizationService authorizationService, + IContentPublishingService contentPublishingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _contentPublishingService = contentPublishingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -28,6 +38,16 @@ public class PublishDocumentWithDescendantsController : DocumentControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task PublishWithDescendants(Guid id, PublishDocumentWithDescendantsRequestModel requestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.Branch(ActionPublish.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt> attempt = await _contentPublishingService.PublishBranchAsync( id, requestModel.Cultures, diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs index 12dc774a8c..53db83fd73 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs @@ -16,7 +16,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin; [ApiController] [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.RecycleBin}/{Constants.UdiEntityType.Document}")] [RequireDocumentTreeRootAccess] -[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Document))] [Authorize(Policy = "New" + AuthorizationPolicies.TreeAccessDocuments)] public class DocumentRecycleBinControllerBase : RecycleBinControllerBase diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/SortDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/SortDocumentController.cs index ce54142c31..80e7bccda9 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/SortDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/SortDocumentController.cs @@ -1,22 +1,32 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Sorting; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class SortDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IContentEditingService _contentEditingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public SortDocumentController(IContentEditingService contentEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public SortDocumentController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _contentEditingService = contentEditingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -28,6 +38,16 @@ public class SortDocumentController : DocumentControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Sort(SortingRequestModel sortingRequestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionSort.ActionLetter, new List(sortingRequestModel.Sorting.Select(x => x.Id).Cast()) { sortingRequestModel.ParentId }), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + ContentEditingOperationStatus result = await _contentEditingService.SortAsync( sortingRequestModel.ParentId, sortingRequestModel.Sorting.Select(m => new SortingModel { Key = m.Id, SortOrder = m.SortOrder }), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/UnpublishDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/UnpublishDocumentController.cs index c507f331e7..7f3aa24641 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/UnpublishDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/UnpublishDocumentController.cs @@ -1,22 +1,32 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class UnpublishDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IContentPublishingService _contentPublishingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public UnpublishDocumentController(IContentPublishingService contentPublishingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public UnpublishDocumentController( + IAuthorizationService authorizationService, + IContentPublishingService contentPublishingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _contentPublishingService = contentPublishingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -28,6 +38,17 @@ public class UnpublishDocumentController : DocumentControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Unpublish(Guid id, UnpublishDocumentRequestModel requestModel) { + + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionUnpublish.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt attempt = await _contentPublishingService.UnpublishAsync( id, requestModel.Culture, diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentController.cs index 9113596482..3d68d3e6d2 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentController.cs @@ -1,29 +1,37 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class UpdateDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IContentEditingService _contentEditingService; private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; public UpdateDocumentController( + IAuthorizationService authorizationService, IContentEditingService contentEditingService, IDocumentEditingPresentationFactory documentEditingPresentationFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _contentEditingService = contentEditingService; _documentEditingPresentationFactory = documentEditingPresentationFactory; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -33,9 +41,18 @@ public class UpdateDocumentController : DocumentControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Update(Guid id, UpdateDocumentRequestModel requestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + IContent? content = await _contentEditingService.GetAsync(id); if (content == null) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdatePublicAccessDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdatePublicAccessDocumentController.cs index 46b56959c0..790169995f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdatePublicAccessDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdatePublicAccessDocumentController.cs @@ -1,25 +1,33 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; using Umbraco.Cms.Api.Management.ViewModels.PublicAccess; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class UpdatePublicAccessDocumentController : DocumentControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IPublicAccessPresentationFactory _publicAccessPresentationFactory; private readonly IPublicAccessService _publicAccessService; public UpdatePublicAccessDocumentController( + IAuthorizationService authorizationService, IPublicAccessPresentationFactory publicAccessPresentationFactory, IPublicAccessService publicAccessService) { + _authorizationService = authorizationService; _publicAccessPresentationFactory = publicAccessPresentationFactory; _publicAccessService = publicAccessService; } @@ -28,9 +36,18 @@ public class UpdatePublicAccessDocumentController : DocumentControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Update(Guid id, PublicAccessRequestModel requestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionProtect.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + PublicAccessEntrySlim publicAccessEntrySlim = _publicAccessPresentationFactory.CreatePublicAccessEntrySlim(requestModel, id); Attempt updateAttempt = await _publicAccessService.UpdateAsync(publicAccessEntrySlim); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs index b019d03635..ea824f40ca 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs @@ -1,19 +1,22 @@ using System.Linq.Expressions; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.Attributes; using Umbraco.Cms.Api.Common.Filters; using Umbraco.Cms.Api.Management.DependencyInjection; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Features; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers; [Authorize(Policy = "New" + AuthorizationPolicies.BackOfficeAccess)] +[Authorize(Policy = "New" + AuthorizationPolicies.UmbracoFeatureEnabled)] [MapToApi(ManagementApiConfiguration.ApiName)] [JsonOptionsName(Constants.JsonOptionsNames.BackOffice)] -public abstract class ManagementApiControllerBase : Controller +public abstract class ManagementApiControllerBase : Controller, IUmbracoFeature { protected CreatedAtActionResult CreatedAtAction(Expression> action, Guid id) => CreatedAtAction(action, new { id = id }); @@ -48,4 +51,14 @@ public abstract class ManagementApiControllerBase : Controller { return backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Key ?? throw new InvalidOperationException("No backoffice user found"); } + + /// + /// Creates a 403 Forbidden result. + /// + /// + /// Use this method instead of on the controller base. + /// This method ensures that a proper 403 Forbidden status code is returned to the client. + /// + // Duplicate code copied between Management API and Delivery API. + protected IActionResult Forbidden() => new StatusCodeResult(StatusCodes.Status403Forbidden); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/ByKeyMediaController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/ByKeyMediaController.cs index f9675bdf1f..5f00a0f843 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/ByKeyMediaController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/ByKeyMediaController.cs @@ -1,22 +1,31 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.Media; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Media; [ApiVersion("1.0")] public class ByKeyMediaController : MediaControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IMediaEditingService _mediaEditingService; private readonly IMediaPresentationModelFactory _mediaPresentationModelFactory; - public ByKeyMediaController(IMediaEditingService mediaEditingService, IMediaPresentationModelFactory mediaPresentationModelFactory) + public ByKeyMediaController( + IAuthorizationService authorizationService, + IMediaEditingService mediaEditingService, + IMediaPresentationModelFactory mediaPresentationModelFactory) { + _authorizationService = authorizationService; _mediaEditingService = mediaEditingService; _mediaPresentationModelFactory = mediaPresentationModelFactory; } @@ -27,6 +36,16 @@ public class ByKeyMediaController : MediaControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task ByKey(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + MediaPermissionResource.WithKeys(id), + AuthorizationPolicies.MediaPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + IMedia? media = await _mediaEditingService.GetAsync(id); if (media == null) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/CreateMediaController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/CreateMediaController.cs index 0b9247e653..5f69fac11a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/CreateMediaController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/CreateMediaController.cs @@ -1,7 +1,9 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.Media; using Umbraco.Cms.Api.Management.ViewModels.Media; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -9,20 +11,28 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Media; [ApiVersion("1.0")] public class CreateMediaController : MediaControllerBase { - private readonly IMediaEditingService _mediaEditingService; + private readonly IAuthorizationService _authorizationService; private readonly IMediaEditingPresentationFactory _mediaEditingPresentationFactory; + private readonly IMediaEditingService _mediaEditingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public CreateMediaController(IMediaEditingService mediaEditingService, IMediaEditingPresentationFactory mediaEditingPresentationFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public CreateMediaController( + IAuthorizationService authorizationService, + IMediaEditingPresentationFactory mediaEditingPresentationFactory, + IMediaEditingService mediaEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { - _mediaEditingService = mediaEditingService; + _authorizationService = authorizationService; _mediaEditingPresentationFactory = mediaEditingPresentationFactory; + _mediaEditingService = mediaEditingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -30,9 +40,19 @@ public class CreateMediaController : MediaControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Create(CreateMediaRequestModel createRequestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + MediaPermissionResource.WithKeys(createRequestModel.ParentId), + AuthorizationPolicies.MediaPermissionByResource); + ; + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + MediaCreateModel model = _mediaEditingPresentationFactory.MapCreateModel(createRequestModel); Attempt result = await _mediaEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor)); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/MoveMediaController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/MoveMediaController.cs index edee3334c0..ace71c738e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/MoveMediaController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/MoveMediaController.cs @@ -1,23 +1,32 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Media; using Umbraco.Cms.Api.Management.ViewModels.Media; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Media; [ApiVersion("1.0")] public class MoveMediaController : MediaControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IMediaEditingService _mediaEditingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public MoveMediaController(IMediaEditingService mediaEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public MoveMediaController( + IAuthorizationService authorizationService, + IMediaEditingService mediaEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _mediaEditingService = mediaEditingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -26,9 +35,18 @@ public class MoveMediaController : MediaControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Move(Guid id, MoveMediaRequestModel moveDocumentRequestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + MediaPermissionResource.WithKeys(new[] { moveDocumentRequestModel.TargetId, id }), + AuthorizationPolicies.MediaPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt result = await _mediaEditingService.MoveAsync( id, moveDocumentRequestModel.TargetId, diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/MoveToRecycleBinMediaController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/MoveToRecycleBinMediaController.cs index 44fcb064b1..8c3d771e6f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/MoveToRecycleBinMediaController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/MoveToRecycleBinMediaController.cs @@ -1,22 +1,31 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Media; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Media; [ApiVersion("1.0")] public class MoveToRecycleBinMediaController : MediaControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IMediaEditingService _mediaEditingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public MoveToRecycleBinMediaController(IMediaEditingService mediaEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public MoveToRecycleBinMediaController( + IAuthorizationService authorizationService, + IMediaEditingService mediaEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _mediaEditingService = mediaEditingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -28,6 +37,16 @@ public class MoveToRecycleBinMediaController : MediaControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task MoveToRecycleBin(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + MediaPermissionResource.WithKeys(id), + AuthorizationPolicies.MediaPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt result = await _mediaEditingService.MoveToRecycleBinAsync(id, CurrentUserKey(_backOfficeSecurityAccessor)); return result.Success ? Ok() diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs index 253097b822..38ca053fc6 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs @@ -16,7 +16,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.RecycleBin; [ApiController] [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.RecycleBin}/{Constants.UdiEntityType.Media}")] [RequireMediaTreeRootAccess] -[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Media))] [Authorize(Policy = "New" + AuthorizationPolicies.SectionAccessMedia)] public class MediaRecycleBinControllerBase : RecycleBinControllerBase diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/SortMediaController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/SortMediaController.cs index cd73051a4c..4fdd085864 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/SortMediaController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/SortMediaController.cs @@ -1,22 +1,31 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Media; using Umbraco.Cms.Api.Management.ViewModels.Sorting; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Media; [ApiVersion("1.0")] public class SortMediaController : MediaControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IMediaEditingService _mediaEditingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public SortMediaController(IMediaEditingService mediaEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public SortMediaController( + IAuthorizationService authorizationService, + IMediaEditingService mediaEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _mediaEditingService = mediaEditingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -28,6 +37,15 @@ public class SortMediaController : MediaControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Sort(SortingRequestModel sortingRequestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + MediaPermissionResource.WithKeys(new List(sortingRequestModel.Sorting.Select(x => x.Id).Cast()) { sortingRequestModel.ParentId }), + AuthorizationPolicies.MediaPermissionByResource); + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + ContentEditingOperationStatus result = await _mediaEditingService.SortAsync( sortingRequestModel.ParentId, sortingRequestModel.Sorting.Select(m => new SortingModel { Key = m.Id, SortOrder = m.SortOrder }), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/UpdateMediaController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/UpdateMediaController.cs index c74e41e235..975039a3f6 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/UpdateMediaController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/UpdateMediaController.cs @@ -1,7 +1,9 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.Media; using Umbraco.Cms.Api.Management.ViewModels.Media; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -9,21 +11,26 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Media; [ApiVersion("1.0")] public class UpdateMediaController : MediaControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IMediaEditingService _mediaEditingService; private readonly IMediaEditingPresentationFactory _mediaEditingPresentationFactory; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; public UpdateMediaController( + IAuthorizationService authorizationService, IMediaEditingService mediaEditingService, IMediaEditingPresentationFactory mediaEditingPresentationFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _mediaEditingService = mediaEditingService; _mediaEditingPresentationFactory = mediaEditingPresentationFactory; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -33,9 +40,18 @@ public class UpdateMediaController : MediaControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Update(Guid id, UpdateMediaRequestModel updateRequestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + MediaPermissionResource.WithKeys(id), + AuthorizationPolicies.MediaPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + IMedia? media = await _mediaEditingService.GetAsync(id); if (media == null) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs index 7f29abd7d6..a880b472c1 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs @@ -2,7 +2,6 @@ using Asp.Versioning; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -45,7 +44,7 @@ public class BackOfficeController : SecurityControllerBase // FIXME: this is a temporary solution to get the new backoffice auth rolling. // once the old backoffice auth is no longer necessary, clean this up and merge with 2FA handling etc. - [AllowAnonymous] + // [AllowAnonymous] // This is handled implicitly by the NewDenyLocalLoginIfConfigured policy on the . Keep it here for now and check FIXME in . [HttpPost("login")] [MapToApiVersion("1.0")] public async Task Login(LoginRequestModel model) @@ -70,7 +69,7 @@ public class BackOfficeController : SecurityControllerBase public required string Password { get; init; } } - [AllowAnonymous] + // [AllowAnonymous] // This is handled implicitly by the NewDenyLocalLoginIfConfigured policy on the . Keep it here for now and check FIXME in . [HttpGet("authorize")] [MapToApiVersion("1.0")] public async Task Authorize() diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordController.cs index a39db00762..01c3c91c2c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordController.cs @@ -19,7 +19,7 @@ public class ResetPasswordController : SecurityControllerBase [HttpPost("forgot-password")] [MapToApiVersion("1.0")] - [AllowAnonymous] + [AllowAnonymous] // This is handled implicitly by the NewDenyLocalLoginIfConfigured policy on the . Keep it here for now and check FIXME in . [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [UserPasswordEnsureMinimumResponseTime] @@ -29,7 +29,7 @@ public class ResetPasswordController : SecurityControllerBase // If this feature is switched off in configuration, the UI will be amended to not make the request to reset password available. // So this is just a server-side secondary check. - // No matter what other status it will just return Ok, so you can't use this endpoint to determine whether the email exists in the system. + // Regardless of other status values, it will just return Ok, so you can't use this endpoint to determine whether the email exists in the system. return result.Result == UserOperationStatus.CannotPasswordReset ? BadRequest() : Ok(); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordTokenController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordTokenController.cs index c67545a4a2..bac2ee1064 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordTokenController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordTokenController.cs @@ -1,5 +1,4 @@ using Asp.Versioning; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.Builders; @@ -21,7 +20,7 @@ public class ResetPasswordTokenController : SecurityControllerBase [HttpPost("forgot-password/reset")] [MapToApiVersion("1.0")] - [AllowAnonymous] + // [AllowAnonymous] // This is handled implicitly by the NewDenyLocalLoginIfConfigured policy on the . Keep it here for now and check FIXME in . [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(ProblemDetailsBuilder), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetailsBuilder), StatusCodes.Status404NotFound)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs index 0da834d8ec..1aa113bded 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs @@ -1,15 +1,18 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Security; [ApiController] [VersionedApiBackOfficeRoute("security")] [ApiExplorerSettings(GroupName = "Security")] +[Authorize(Policy = "New" + AuthorizationPolicies.DenyLocalLoginIfConfigured)] public abstract class SecurityControllerBase : ManagementApiControllerBase { protected IActionResult UserOperationStatusResult(UserOperationStatus status, ErrorMessageResult? errorMessageResult = null) => diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/VerifyResetPasswordTokenController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/VerifyResetPasswordTokenController.cs index f0b83bb001..f806a62f79 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/VerifyResetPasswordTokenController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/VerifyResetPasswordTokenController.cs @@ -1,5 +1,4 @@ using Asp.Versioning; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.Builders; @@ -20,7 +19,7 @@ public class VerifyResetPasswordTokenController : SecurityControllerBase [HttpPost("forgot-password/verify")] [MapToApiVersion("1.0")] - [AllowAnonymous] + // [AllowAnonymous] // This is handled implicitly by the NewDenyLocalLoginIfConfigured policy on the . Keep it here for now and check FIXME in . [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(ProblemDetailsBuilder), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetailsBuilder), StatusCodes.Status404NotFound)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/BulkDeleteUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/BulkDeleteUsersController.cs index 1e90e9c710..0dec94972b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/BulkDeleteUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/BulkDeleteUsersController.cs @@ -1,21 +1,30 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] public class BulkDeleteUsersController : UserControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserService _userService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public BulkDeleteUsersController(IUserService userService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public BulkDeleteUsersController( + IAuthorizationService authorizationService, + IUserService userService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _userService = userService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -26,6 +35,16 @@ public class BulkDeleteUsersController : UserControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task DeleteUsers(DeleteUsersRequestModel model) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(model.UserIds), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + UserOperationStatus result = await _userService.DeleteAsync(CurrentUserKey(_backOfficeSecurityAccessor), model.UserIds); return result is UserOperationStatus.Success diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ByKeyUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ByKeyUsersController.cs index 1a372ea6d7..d60a71a78e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/ByKeyUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ByKeyUsersController.cs @@ -1,24 +1,31 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] public class ByKeyUserController : UserControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserService _userService; private readonly IUserPresentationFactory _userPresentationFactory; public ByKeyUserController( + IAuthorizationService authorizationService, IUserService userService, IUserPresentationFactory userPresentationFactory) { + _authorizationService = authorizationService; _userService = userService; _userPresentationFactory = userPresentationFactory; } @@ -29,6 +36,16 @@ public class ByKeyUserController : UserControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task ByKey(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(id), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + IUser? user = await _userService.GetAsync(id); if (user is null) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ClearAvatarUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ClearAvatarUsersController.cs index f489a98bf4..95b8bbd58f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/ClearAvatarUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ClearAvatarUsersController.cs @@ -1,21 +1,40 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] public class ClearAvatarUserController : UserControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserService _userService; - public ClearAvatarUserController(IUserService userService) => _userService = userService; + public ClearAvatarUserController(IAuthorizationService authorizationService, IUserService userService) + { + _authorizationService = authorizationService; + _userService = userService; + } [MapToApiVersion("1.0")] [HttpDelete("avatar/{id:guid}")] public async Task ClearAvatar(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(id), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + UserOperationStatus result = await _userService.ClearAvatarAsync(id); return result is UserOperationStatus.Success diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/CreateInitialPasswordUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/CreateInitialPasswordUserController.cs index 2385a9f680..a53d13dfd8 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/CreateInitialPasswordUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/CreateInitialPasswordUserController.cs @@ -7,17 +7,19 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] +[Authorize(Policy = "New" + AuthorizationPolicies.DenyLocalLoginIfConfigured)] public class CreateInitialPasswordUserController : UserControllerBase { private readonly IUserService _userService; public CreateInitialPasswordUserController(IUserService userService) => _userService = userService; - [AllowAnonymous] + // [AllowAnonymous] // This is handled implicitly by the NewDenyLocalLoginIfConfigured policy on the . Keep it here for now and check FIXME in . [HttpPost("invite/create-password")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetCurrentUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetCurrentUserController.cs index 6e70c7e2e4..46e3436787 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetCurrentUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/GetCurrentUserController.cs @@ -1,11 +1,15 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User.Current; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User.Current; @@ -13,15 +17,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.User.Current; public class GetCurrentUserController : CurrentUserControllerBase { private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IAuthorizationService _authorizationService; private readonly IUserService _userService; private readonly IUserPresentationFactory _userPresentationFactory; public GetCurrentUserController( IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IAuthorizationService authorizationService, IUserService userService, IUserPresentationFactory userPresentationFactory) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _authorizationService = authorizationService; _userService = userService; _userPresentationFactory = userPresentationFactory; } @@ -33,6 +40,16 @@ public class GetCurrentUserController : CurrentUserControllerBase { var currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor); + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(currentUserKey), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + IUser? user = await _userService.GetAsync(currentUserKey); if (user is null) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/SetAvatarCurrentUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/SetAvatarCurrentUserController.cs index 9958ef2e17..1431f479f3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/Current/SetAvatarCurrentUserController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/Current/SetAvatarCurrentUserController.cs @@ -1,25 +1,32 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User.Current; [ApiVersion("1.0")] public class SetAvatarCurrentUserController : CurrentUserControllerBase { - private readonly IUserService _userService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IAuthorizationService _authorizationService; + private readonly IUserService _userService; public SetAvatarCurrentUserController( - IUserService userService, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IAuthorizationService authorizationService, + IUserService userService) { - _userService = userService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _authorizationService = authorizationService; + _userService = userService; } [MapToApiVersion("1.0")] @@ -29,6 +36,16 @@ public class SetAvatarCurrentUserController : CurrentUserControllerBase { Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor); + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(userKey), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + UserOperationStatus result = await _userService.SetAvatarAsync(userKey, model.FileId); return result is UserOperationStatus.Success diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/DeleteUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/DeleteUsersController.cs index c71a933dfb..558267f9c9 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/DeleteUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/DeleteUsersController.cs @@ -1,27 +1,46 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] public class DeleteUserController : UserControllerBase { - public DeleteUserController(IUserService userService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + private readonly IAuthorizationService _authorizationService; + private readonly IUserService _userService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public DeleteUserController( + IAuthorizationService authorizationService, + IUserService userService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _userService = userService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } - private readonly IUserService _userService; - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - [MapToApiVersion("1.0")] [HttpDelete("{id:guid}")] public async Task DeleteUser(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(id), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + UserOperationStatus result = await _userService.DeleteAsync(CurrentUserKey(_backOfficeSecurityAccessor), id); return result is UserOperationStatus.Success diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/DisableUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/DisableUsersController.cs index 9d74715e61..4a820202e9 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/DisableUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/DisableUsersController.cs @@ -1,21 +1,30 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] public class DisableUserController : UserControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserService _userService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public DisableUserController(IUserService userService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public DisableUserController( + IAuthorizationService authorizationService, + IUserService userService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _userService = userService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -26,6 +35,16 @@ public class DisableUserController : UserControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task DisableUsers(DisableUserRequestModel model) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(model.UserIds), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + UserOperationStatus result = await _userService.DisableAsync(CurrentUserKey(_backOfficeSecurityAccessor), model.UserIds); return result is UserOperationStatus.Success diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/EnableUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/EnableUsersController.cs index 2aa6db542d..1f62b57ec3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/EnableUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/EnableUsersController.cs @@ -1,21 +1,30 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] public class EnableUserController : UserControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserService _userService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public EnableUserController(IUserService userService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public EnableUserController( + IAuthorizationService authorizationService, + IUserService userService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _userService = userService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -26,6 +35,16 @@ public class EnableUserController : UserControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task EnableUsers(EnableUserRequestModel model) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(model.UserIds), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + UserOperationStatus result = await _userService.EnableAsync(CurrentUserKey(_backOfficeSecurityAccessor), model.UserIds); return result is UserOperationStatus.Success diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/SetAvatarUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/SetAvatarUsersController.cs index 497b123239..4265183591 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/SetAvatarUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/SetAvatarUsersController.cs @@ -1,19 +1,25 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] public class SetAvatarUserController : UserControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserService _userService; - public SetAvatarUserController(IUserService userService) + public SetAvatarUserController(IUserService userService, IAuthorizationService authorizationService) { + _authorizationService = authorizationService; _userService = userService; } @@ -23,6 +29,16 @@ public class SetAvatarUserController : UserControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task SetAvatar(Guid id, SetAvatarRequestModel model) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(id), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + UserOperationStatus result = await _userService.SetAvatarAsync(id, model.FileId); return result is UserOperationStatus.Success diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/UnlockUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/UnlockUsersController.cs index 231c6a72fd..0b079dd0cc 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/UnlockUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/UnlockUsersController.cs @@ -1,23 +1,32 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] public class UnlockUserController : UserControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserService _userService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public UnlockUserController(IUserService userService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public UnlockUserController( + IAuthorizationService authorizationService, + IUserService userService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + _authorizationService = authorizationService; _userService = userService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } @@ -28,6 +37,16 @@ public class UnlockUserController : UserControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task UnlockUsers(UnlockUsersRequestModel model) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(model.UserIds), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt attempt = await _userService.UnlockAsync(CurrentUserKey(_backOfficeSecurityAccessor), model.UserIds.ToArray()); return attempt.Success diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/UpdateUserGroupsUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/UpdateUserGroupsUsersController.cs index eb0a7d0b4f..9771032649 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/UpdateUserGroupsUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/UpdateUserGroupsUsersController.cs @@ -1,18 +1,24 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.User; using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] public class UpdateUserGroupsUserController : UserControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserGroupService _userGroupService; - public UpdateUserGroupsUserController(IUserGroupService userGroupService) + public UpdateUserGroupsUserController(IAuthorizationService authorizationService, IUserGroupService userGroupService) { + _authorizationService = authorizationService; _userGroupService = userGroupService; } @@ -21,6 +27,16 @@ public class UpdateUserGroupsUserController : UserControllerBase [ProducesResponseType(StatusCodes.Status200OK)] public async Task UpdateUserGroups(UpdateUserGroupsOnUserRequestModel requestModel) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(requestModel.UserIds), + AuthorizationPolicies.AdminUserEditsRequireAdmin); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + await _userGroupService.UpdateUserGroupsOnUsers(requestModel.UserGroupIds, requestModel.UserIds); return Ok(); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/UsersControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/UsersControllerBase.cs index fff765c79b..46a66535d5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/UsersControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/UsersControllerBase.cs @@ -124,7 +124,7 @@ public abstract class UserControllerBase : ManagementApiControllerBase .WithTitle("Invalid user state") .WithDetail("The target user is not in the invite state.") .Build()), - UserOperationStatus.Forbidden => Forbid(), + UserOperationStatus.Forbidden => Forbidden(), _ => StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetailsBuilder() .WithTitle("Unknown user operation status.") .Build()), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/VerifyInviteUsersController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/VerifyInviteUsersController.cs index 5dff651a46..7fc30d6d7d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/VerifyInviteUsersController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/VerifyInviteUsersController.cs @@ -6,17 +6,19 @@ using Umbraco.Cms.Api.Management.ViewModels.User; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.User; [ApiVersion("1.0")] +[Authorize(Policy = "New" + AuthorizationPolicies.DenyLocalLoginIfConfigured)] public class VerifyInviteUserController : UserControllerBase { private readonly IUserService _userService; public VerifyInviteUserController(IUserService userService) => _userService = userService; - [AllowAnonymous] + // [AllowAnonymous] // This is handled implicitly by the NewDenyLocalLoginIfConfigured policy. Keep it here for now and check FIXME in . [HttpPost("invite/verify")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/BulkDeleteUserGroupsController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/BulkDeleteUserGroupsController.cs index 1acfc6f3b8..2059d7c327 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/BulkDeleteUserGroupsController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/BulkDeleteUserGroupsController.cs @@ -1,20 +1,26 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.UserGroup; using Umbraco.Cms.Api.Management.ViewModels.UserGroup; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.UserGroup; [ApiVersion("1.0")] public class BulkDeleteUserGroupsController : UserGroupControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserGroupService _userGroupService; - public BulkDeleteUserGroupsController(IUserGroupService userGroupService) + public BulkDeleteUserGroupsController(IAuthorizationService authorizationService, IUserGroupService userGroupService) { + _authorizationService = authorizationService; _userGroupService = userGroupService; } @@ -24,6 +30,16 @@ public class BulkDeleteUserGroupsController : UserGroupControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task BulkDelete(DeleteUserGroupsRequestModel model) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + new UserGroupPermissionResource(model.UserGroupIds), + AuthorizationPolicies.UserBelongsToUserGroupInRequest); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt result = await _userGroupService.DeleteAsync(model.UserGroupIds); return result.Success diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/ByKeyUserGroupController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/ByKeyUserGroupController.cs index 2b2c859cfd..9ae44ac9f0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/ByKeyUserGroupController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/ByKeyUserGroupController.cs @@ -1,23 +1,30 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security.Authorization.UserGroup; using Umbraco.Cms.Api.Management.ViewModels.UserGroup; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.UserGroup; [ApiVersion("1.0")] public class ByKeyUserGroupController : UserGroupControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserGroupService _userGroupService; private readonly IUserGroupPresentationFactory _userGroupPresentationFactory; public ByKeyUserGroupController( + IAuthorizationService authorizationService, IUserGroupService userGroupService, IUserGroupPresentationFactory userGroupPresentationFactory) { + _authorizationService = authorizationService; _userGroupService = userGroupService; _userGroupPresentationFactory = userGroupPresentationFactory; } @@ -28,6 +35,16 @@ public class ByKeyUserGroupController : UserGroupControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task ByKey(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserGroupPermissionResource.WithKeys(id), + AuthorizationPolicies.UserBelongsToUserGroupInRequest); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + IUserGroup? userGroup = await _userGroupService.GetAsync(id); if (userGroup is null) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/DeleteUserGroupController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/DeleteUserGroupController.cs index 930655f391..5794ad8b29 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/DeleteUserGroupController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/DeleteUserGroupController.cs @@ -1,19 +1,25 @@ using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.UserGroup; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.UserGroup; [ApiVersion("1.0")] public class DeleteUserGroupController : UserGroupControllerBase { + private readonly IAuthorizationService _authorizationService; private readonly IUserGroupService _userGroupService; - public DeleteUserGroupController(IUserGroupService userGroupService) + public DeleteUserGroupController(IAuthorizationService authorizationService, IUserGroupService userGroupService) { + _authorizationService = authorizationService; _userGroupService = userGroupService; } @@ -23,6 +29,16 @@ public class DeleteUserGroupController : UserGroupControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Delete(Guid id) { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserGroupPermissionResource.WithKeys(id), + AuthorizationPolicies.UserBelongsToUserGroupInRequest); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + Attempt result = await _userGroupService.DeleteAsync(id); return result.Success diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupsControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupsControllerBase.cs index 7f920d1764..334d7ea0ba 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupsControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupsControllerBase.cs @@ -28,7 +28,7 @@ public class UserGroupControllerBase : ManagementApiControllerBase .Build()), UserGroupOperationStatus.MissingUser => Unauthorized(new ProblemDetailsBuilder() .WithTitle("Missing user") - .WithDetail("A performing user was not found when attempting to create the user group.") + .WithDetail("A performing user was not found when attempting the operation.") .Build()), UserGroupOperationStatus.IsSystemUserGroup => BadRequest(new ProblemDetailsBuilder() .WithTitle("System user group") diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs index 1a1def8cc4..d816a02881 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs @@ -1,10 +1,18 @@ -using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using OpenIddict.Validation.AspNetCore; +using Umbraco.Cms.Api.Management.Security.Authorization; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; +using Umbraco.Cms.Api.Management.Security.Authorization.DenyLocalLogin; +using Umbraco.Cms.Api.Management.Security.Authorization.Feature; +using Umbraco.Cms.Api.Management.Security.Authorization.Media; +using Umbraco.Cms.Api.Management.Security.Authorization.User; +using Umbraco.Cms.Api.Management.Security.Authorization.UserGroup; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Web.Common.Authorization; +using FeatureAuthorizeHandler = Umbraco.Cms.Api.Management.Security.Authorization.Feature.FeatureAuthorizeHandler; +using FeatureAuthorizeRequirement = Umbraco.Cms.Api.Management.Security.Authorization.Feature.FeatureAuthorizeRequirement; namespace Umbraco.Cms.Api.Management.DependencyInjection; @@ -12,6 +20,22 @@ internal static class BackOfficeAuthPolicyBuilderExtensions { internal static IUmbracoBuilder AddAuthorizationPolicies(this IUmbracoBuilder builder) { + // NOTE: Even though we are registering these handlers globally they will only actually execute their logic for + // any auth defining a matching requirement and scheme. + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddAuthorization(CreatePolicies); return builder; } @@ -33,6 +57,12 @@ internal static class BackOfficeAuthPolicyBuilderExtensions policy.RequireAuthenticatedUser(); }); + options.AddPolicy($"New{AuthorizationPolicies.RequireAdminAccess}", policy => + { + policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + policy.RequireRole(Constants.Security.AdminGroupAlias); + }); + AddPolicy(AuthorizationPolicies.SectionAccessContent, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content); AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content, Constants.Applications.Media); AddPolicy(AuthorizationPolicies.SectionAccessForContentTree, Constants.Security.AllowedApplicationsClaimType, @@ -65,6 +95,42 @@ internal static class BackOfficeAuthPolicyBuilderExtensions AddPolicy(AuthorizationPolicies.TreeAccessStylesheets, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); AddPolicy(AuthorizationPolicies.TreeAccessTemplates, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.RequireAdminAccess, ClaimsIdentity.DefaultRoleClaimType, Constants.Security.AdminGroupAlias); + // Contextual permissions + // TODO: Rename policies once we have the old ones removed + options.AddPolicy($"New{AuthorizationPolicies.AdminUserEditsRequireAdmin}", policy => + { + policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + policy.Requirements.Add(new UserPermissionRequirement()); + }); + + options.AddPolicy($"New{AuthorizationPolicies.ContentPermissionByResource}", policy => + { + policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + policy.Requirements.Add(new ContentPermissionRequirement()); + }); + + options.AddPolicy($"New{AuthorizationPolicies.DenyLocalLoginIfConfigured}", policy => + { + policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + policy.Requirements.Add(new DenyLocalLoginRequirement()); + }); + + options.AddPolicy($"New{AuthorizationPolicies.MediaPermissionByResource}", policy => + { + policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + policy.Requirements.Add(new MediaPermissionRequirement()); + }); + + options.AddPolicy($"New{AuthorizationPolicies.UmbracoFeatureEnabled}", policy => + { + policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + policy.Requirements.Add(new FeatureAuthorizeRequirement()); + }); + + options.AddPolicy($"New{AuthorizationPolicies.UserBelongsToUserGroupInRequest}", policy => + { + policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + policy.Requirements.Add(new UserGroupPermissionRequirement()); + }); } } diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..aa47c36e14 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs @@ -0,0 +1,88 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Common.Configuration; +using Umbraco.Cms.Api.Common.DependencyInjection; +using Umbraco.Cms.Api.Management.Configuration; +using Umbraco.Cms.Api.Management.DependencyInjection; +using Umbraco.Cms.Api.Management.Serialization; +using Umbraco.Cms.Api.Management.Services; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models.Configuration; +using Umbraco.Cms.Web.Common.ApplicationBuilder; + +namespace Umbraco.Extensions; + +public static class UmbracoBuilderExtensions +{ + public static IUmbracoBuilder AddUmbracoManagementApi(this IUmbracoBuilder builder) + { + IServiceCollection services = builder.Services; + + if (!services.Any(x => x.ImplementationType == typeof(JsonPatchService))) + { + ModelsBuilderBuilderExtensions.AddModelsBuilder(builder) + .AddJson() + .AddNewInstaller() + .AddUpgrader() + .AddSearchManagement() + .AddTrees() + .AddAuditLogs() + .AddDocuments() + .AddDocumentTypes() + .AddMedia() + .AddMediaTypes() + .AddLanguages() + .AddDictionary() + .AddHealthChecks() + .AddRedirectUrl() + .AddTags() + .AddTrackedReferences() + .AddTemporaryFiles() + .AddDataTypes() + .AddTemplates() + .AddRelationTypes() + .AddLogViewer() + .AddUsers() + .AddUserGroups() + .AddTours() + .AddPackages() + .AddEntities() + .AddPathFolders() + .AddScripts() + .AddPartialViews() + .AddStylesheets() + .AddServer() + .AddCorsPolicy() + .AddBackOfficeAuthentication(); + + services + .ConfigureOptions() + .AddControllers() + .AddJsonOptions(_ => + { + // any generic JSON options go here + }) + .AddJsonOptions(Constants.JsonOptionsNames.BackOffice, _ => { }); + + services.ConfigureOptions(); + services.ConfigureOptions(); + + services.Configure(options => + { + options.AddFilter(new UmbracoPipelineFilter( + "BackOfficeManagementApiFilter", + applicationBuilder => applicationBuilder.UseProblemDetailsExceptionHandling(), + applicationBuilder => { }, + applicationBuilder => applicationBuilder.UseEndpoints())); + }); + + // FIXME: when this is moved to core, make the AddUmbracoOptions extension private again and remove core InternalsVisibleTo for Umbraco.Cms.Api.Management + builder.AddUmbracoOptions(); + // FIXME: remove this when NewBackOfficeSettings is moved to core + services.AddSingleton, NewBackOfficeSettingsValidator>(); + } + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs index b218774fb8..77f2740c1e 100644 --- a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs @@ -1,86 +1,11 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Api.Common.Configuration; -using Umbraco.Cms.Api.Common.DependencyInjection; -using Umbraco.Cms.Api.Management.Configuration; -using Umbraco.Cms.Api.Management.DependencyInjection; -using Umbraco.Cms.Api.Management.Serialization; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models.Configuration; -using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management; public class ManagementApiComposer : IComposer { - public void Compose(IUmbracoBuilder builder) - { - // TODO Should just call a single extension method that can be called fromUmbracoTestServerTestBase too, instead of calling this method - - IServiceCollection services = builder.Services; - - ModelsBuilderBuilderExtensions.AddModelsBuilder(builder - .AddJson() - .AddNewInstaller() - .AddUpgrader() - .AddSearchManagement() - .AddTrees() - .AddAuditLogs() - .AddDocuments() - .AddDocumentTypes() - .AddMedia() - .AddMediaTypes() - .AddLanguages() - .AddDictionary() - .AddHealthChecks()) - .AddRedirectUrl() - .AddTags() - .AddTrackedReferences() - .AddTemporaryFiles() - .AddDataTypes() - .AddTemplates() - .AddRelationTypes() - .AddLogViewer() - .AddUsers() - .AddUserGroups() - .AddTours() - .AddPackages() - .AddEntities() - .AddPathFolders() - .AddScripts() - .AddPartialViews() - .AddStylesheets() - .AddServer() - .AddCorsPolicy() - .AddBackOfficeAuthentication(); - - services - .ConfigureOptions() - .AddControllers() - .AddJsonOptions(_ => - { - // any generic JSON options go here - }) - .AddJsonOptions(Constants.JsonOptionsNames.BackOffice, _ => { }); - - services.ConfigureOptions( ); - services.ConfigureOptions( ); - - services.Configure(options => - { - options.AddFilter(new UmbracoPipelineFilter( - "BackOfficeManagementApiFilter", - applicationBuilder => applicationBuilder.UseProblemDetailsExceptionHandling(), - applicationBuilder => { }, - applicationBuilder => applicationBuilder.UseEndpoints())); - }); - - // FIXME: when this is moved to core, make the AddUmbracoOptions extension private again and remove core InternalsVisibleTo for Umbraco.Cms.Api.Management - builder.AddUmbracoOptions(); - // FIXME: remove this when NewBackOfficeSettings is moved to core - services.AddSingleton, NewBackOfficeSettingsValidator>(); - } + public void Compose(IUmbracoBuilder builder) => builder.AddUmbracoManagementApi(); } diff --git a/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs b/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs index bd5395d0d6..6a47d378dc 100644 --- a/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs +++ b/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs @@ -10,8 +10,8 @@ namespace Umbraco.Cms.Api.Management.Middleware; public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware { - private static bool _firstBackOfficeRequest; - private static SemaphoreSlim _firstBackOfficeRequestLocker = new(1); + private bool _firstBackOfficeRequest; // this only works because this is a singleton + private SemaphoreSlim _firstBackOfficeRequestLocker = new(1); // this only works because this is a singleton private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly IServiceProvider _serviceProvider; diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index ecc661fa3c..2220acb3b8 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -67,6 +67,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -146,6 +149,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -217,6 +223,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -272,6 +281,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -371,6 +383,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -449,6 +464,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -516,6 +534,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -614,6 +635,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -704,6 +728,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -770,6 +797,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -850,6 +880,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -937,6 +970,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1036,6 +1072,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1114,6 +1153,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1181,6 +1223,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1279,6 +1324,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1349,6 +1397,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1406,6 +1457,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1477,6 +1531,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1540,6 +1597,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1595,6 +1655,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1712,6 +1775,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1790,6 +1856,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1857,6 +1926,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -1955,6 +2027,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2032,6 +2107,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2132,6 +2210,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2231,6 +2312,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2301,6 +2385,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2364,6 +2451,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2419,6 +2509,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2489,6 +2582,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2544,6 +2640,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2643,6 +2742,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2721,6 +2823,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2768,6 +2873,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2866,6 +2974,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -2965,6 +3076,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -3043,6 +3157,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -3110,6 +3227,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -3208,6 +3328,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -3278,6 +3401,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -3349,6 +3475,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -3412,6 +3541,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -3491,6 +3623,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -3569,6 +3707,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -3647,6 +3791,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -3737,6 +3887,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -3815,6 +3971,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -3873,6 +4032,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -3953,6 +4115,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -4022,6 +4190,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -4109,6 +4283,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -4187,6 +4367,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -4277,6 +4460,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -4324,6 +4513,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -4368,6 +4563,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -4446,6 +4647,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -4555,6 +4762,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -4655,6 +4868,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -4755,6 +4974,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -4838,6 +5063,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -4923,6 +5151,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5012,6 +5243,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -5056,9 +5293,6 @@ } ], "responses": { - "401": { - "description": "Unauthorized" - }, "200": { "description": "Success", "content": { @@ -5078,6 +5312,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5114,9 +5351,6 @@ } ], "responses": { - "401": { - "description": "Unauthorized" - }, "200": { "description": "Success", "content": { @@ -5136,6 +5370,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5214,6 +5451,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5284,6 +5524,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5339,6 +5582,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5416,6 +5662,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5493,6 +5742,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5591,6 +5843,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5688,6 +5943,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5743,6 +6001,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5820,6 +6081,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -5925,6 +6189,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6210,6 +6477,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6307,6 +6577,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6384,6 +6657,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6450,6 +6726,9 @@ }, "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6547,6 +6826,9 @@ }, "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6616,6 +6898,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6671,6 +6956,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6756,6 +7044,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6851,6 +7142,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6942,6 +7236,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -6997,6 +7294,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7074,6 +7374,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7151,6 +7454,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7197,6 +7503,9 @@ }, "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7253,6 +7562,9 @@ }, "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7352,6 +7664,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7430,6 +7745,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7477,6 +7795,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7575,6 +7896,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7674,6 +7998,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7752,6 +8079,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7819,6 +8149,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7917,6 +8250,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -7987,6 +8323,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -8058,6 +8397,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -8121,6 +8463,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -8200,6 +8545,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -8278,6 +8629,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -8356,6 +8713,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -8436,6 +8799,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -8505,6 +8874,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -8583,6 +8958,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -8672,6 +9050,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -8716,9 +9100,6 @@ } ], "responses": { - "401": { - "description": "Unauthorized" - }, "200": { "description": "Success", "content": { @@ -8738,6 +9119,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -8774,9 +9158,6 @@ } ], "responses": { - "401": { - "description": "Unauthorized" - }, "200": { "description": "Success", "content": { @@ -8796,6 +9177,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -8867,6 +9251,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -8944,6 +9331,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9007,6 +9397,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9077,6 +9470,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9132,6 +9528,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9202,6 +9601,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9257,6 +9659,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9327,6 +9732,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9365,6 +9773,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9412,6 +9823,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9459,6 +9873,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9514,6 +9931,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9582,6 +10002,9 @@ }, "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9637,6 +10060,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9734,6 +10160,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9812,6 +10241,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9859,6 +10291,9 @@ }, "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -9937,6 +10372,9 @@ }, "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10006,6 +10444,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10062,6 +10503,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10117,6 +10561,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10173,6 +10620,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10230,6 +10680,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10255,6 +10708,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10302,6 +10758,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10329,6 +10788,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10376,6 +10838,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10401,6 +10866,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10470,6 +10938,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10525,6 +10996,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10602,6 +11076,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10664,6 +11141,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10719,6 +11199,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10766,6 +11249,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10813,6 +11299,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10885,6 +11374,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10903,6 +11395,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10921,6 +11416,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10939,6 +11437,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -10974,6 +11475,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11056,6 +11560,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11120,6 +11627,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11147,6 +11657,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11194,6 +11707,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11219,6 +11735,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11298,6 +11817,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11376,6 +11898,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11423,6 +11948,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11550,6 +12078,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11620,6 +12151,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11675,6 +12209,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11747,6 +12284,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11831,6 +12371,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11887,6 +12430,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11944,6 +12490,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -11969,6 +12518,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12016,6 +12568,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12043,6 +12598,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12090,6 +12648,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12115,6 +12676,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12184,6 +12748,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12246,6 +12813,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12301,6 +12871,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12356,6 +12929,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12446,6 +13022,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12623,8 +13202,16 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } - } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] } }, "/umbraco/management/api/v1/security/forgot-password/verify": { @@ -12740,8 +13327,16 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } - } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] } }, "/umbraco/management/api/v1/server/information": { @@ -12782,6 +13377,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12911,6 +13509,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -12980,6 +13581,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13042,6 +13646,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13097,6 +13704,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13153,6 +13763,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13210,6 +13823,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13235,6 +13851,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13282,6 +13901,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13337,6 +13959,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13364,6 +13989,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13411,6 +14039,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13436,6 +14067,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13505,6 +14139,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13583,6 +14220,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13661,6 +14301,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13726,6 +14369,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13788,6 +14434,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13843,6 +14492,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13919,6 +14571,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -13974,6 +14629,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14021,6 +14679,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14088,6 +14749,9 @@ }, "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14187,6 +14851,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14265,6 +14932,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14332,6 +15002,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14430,6 +15103,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14500,6 +15176,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14578,6 +15257,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14625,6 +15307,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14702,6 +15387,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14765,6 +15453,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14820,6 +15511,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14895,6 +15589,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -14993,6 +15690,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15060,6 +15760,9 @@ }, "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15107,6 +15810,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15154,6 +15860,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15226,6 +15935,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15298,6 +16010,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15373,6 +16088,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15431,6 +16149,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15498,6 +16219,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15567,6 +16291,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -15644,6 +16374,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15697,6 +16430,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15775,6 +16511,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -15822,6 +16564,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -15900,6 +16648,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -15970,6 +16721,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -16077,6 +16831,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -16144,6 +16901,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -16197,6 +16960,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -16275,6 +17041,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -16302,6 +17074,12 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -16360,6 +17138,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -16389,6 +17170,12 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -16467,6 +17254,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -16527,6 +17320,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -16574,6 +17370,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -16623,6 +17425,12 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -16672,6 +17480,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -16719,6 +17530,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -16789,6 +17603,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -16859,6 +17676,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -16929,6 +17749,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -16998,6 +17821,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -17067,6 +17896,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -17150,6 +17985,9 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -17209,6 +18047,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -17278,8 +18119,16 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } - } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] } }, "/umbraco/management/api/v1/user/invite/resend": { @@ -17342,6 +18191,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -17420,8 +18272,16 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } - } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] } }, "/umbraco/management/api/v1/user/item": { @@ -17485,6 +18345,9 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" } }, "security": [ @@ -17534,6 +18397,12 @@ "responses": { "200": { "description": "Success" + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -17603,6 +18472,12 @@ } } } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" } }, "security": [ @@ -23155,11 +24030,17 @@ "format": "uuid", "nullable": true }, + "documentRootAccess": { + "type": "boolean" + }, "mediaStartNodeId": { "type": "string", "format": "uuid", "nullable": true }, + "mediaRootAccess": { + "type": "boolean" + }, "permissions": { "uniqueItems": true, "type": "array", @@ -23196,6 +24077,9 @@ "id": { "type": "string", "format": "uuid" + }, + "isSystemGroup": { + "type": "boolean" } }, "additionalProperties": false diff --git a/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilter.cs b/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilter.cs index 4ae470704d..05950a8d5e 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilter.cs +++ b/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilter.cs @@ -1,9 +1,7 @@ - using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -using Umbraco.Cms.Api.Common.Attributes; using Umbraco.Cms.Api.Management.DependencyInjection; using Umbraco.Extensions; @@ -21,13 +19,20 @@ internal class BackOfficeSecurityRequirementsOperationFilter : IOperationFilter if (!context.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) && !(context.MethodInfo.DeclaringType?.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) ?? false)) { + operation.Responses.Add(StatusCodes.Status401Unauthorized.ToString(), new OpenApiResponse + { + Description = "The resource is protected and requires an authentication token" + }); + operation.Security = new List { new OpenApiSecurityRequirement { { - new OpenApiSecurityScheme { - Reference = new OpenApiReference { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { Type = ReferenceType.SecurityScheme, Id = ManagementApiConfiguration.ApiSecurityName } @@ -36,7 +41,16 @@ internal class BackOfficeSecurityRequirementsOperationFilter : IOperationFilter } }; } + + + // If method/controller has an explicit AuthorizeAttribute or the controller ctor injects IAuthorizationService, then we know Forbid result is possible. + if (context.MethodInfo.GetCustomAttributes(false).Any(x => x is AuthorizeAttribute + || context.MethodInfo.DeclaringType?.GetConstructors().Any(x => x.GetParameters().Any(x => x.ParameterType == typeof(IAuthorizationService))) is true)) + { + operation.Responses.Add(StatusCodes.Status403Forbidden.ToString(), new OpenApiResponse + { + Description = "The authenticated user do not have access to this resource" + }); + } } - - } diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/AuthorizationHelper.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/AuthorizationHelper.cs new file mode 100644 index 0000000000..7d5cf31008 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/AuthorizationHelper.cs @@ -0,0 +1,48 @@ +using System.Security.Claims; +using System.Security.Principal; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization; + +/// +internal sealed class AuthorizationHelper : IAuthorizationHelper +{ + private readonly IUserService _userService; + + /// + /// Initializes a new instance of the class. + /// + /// Service for user related operations. + public AuthorizationHelper(IUserService userService) + => _userService = userService; + + /// + public IUser GetUmbracoUser(IPrincipal currentUser) + { + IUser? user = null; + ClaimsIdentity? umbIdentity = currentUser.GetUmbracoIdentity(); + Guid? currentUserKey = umbIdentity?.GetUserKey(); + + if (currentUserKey is null) + { + var currentUserId = umbIdentity?.GetUserId(); + if (currentUserId.HasValue) + { + user = _userService.GetUserById(currentUserId.Value); + } + } + else + { + user = _userService.GetAsync(currentUserKey.Value).GetAwaiter().GetResult(); + } + + if (user is null) + { + throw new InvalidOperationException($"Could not obtain an {nameof(IUser)} instance from {nameof(IPrincipal)}"); + } + + return user; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/AuthorizationServiceExtensions.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/AuthorizationServiceExtensions.cs new file mode 100644 index 0000000000..c5dae101ce --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/AuthorizationServiceExtensions.cs @@ -0,0 +1,11 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Cms.Api.Management.Security.Authorization; + +namespace Umbraco.Extensions; + +public static class AuthorizationServiceExtensions +{ + public static Task AuthorizeResourceAsync(this IAuthorizationService authorizationService, ClaimsPrincipal user, IPermissionResource resource, string policyName) + => authorizationService.AuthorizeAsync(user, resource, $"New{policyName}"); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionAuthorizer.cs new file mode 100644 index 0000000000..ca4aff3a10 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionAuthorizer.cs @@ -0,0 +1,65 @@ +using System.Security.Principal; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.AuthorizationStatus; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Content; + +/// +internal sealed class ContentPermissionAuthorizer : IContentPermissionAuthorizer +{ + private readonly IAuthorizationHelper _authorizationHelper; + private readonly IContentPermissionService _contentPermissionService; + + public ContentPermissionAuthorizer(IAuthorizationHelper authorizationHelper, IContentPermissionService contentPermissionService) + { + _authorizationHelper = authorizationHelper; + _contentPermissionService = contentPermissionService; + } + + /// + public async Task IsAuthorizedAsync(IPrincipal currentUser, IEnumerable contentKeys, ISet permissionsToCheck) + { + if (!contentKeys.Any()) + { + // Must succeed this requirement since we cannot process it. + return true; + } + + IUser user = _authorizationHelper.GetUmbracoUser(currentUser); + + var result = await _contentPermissionService.AuthorizeAccessAsync(user, contentKeys, permissionsToCheck); + + return result == ContentAuthorizationStatus.Success; + } + + /// + public async Task IsAuthorizedWithDescendantsAsync(IPrincipal currentUser, Guid parentKey, ISet permissionsToCheck) + { + IUser user = _authorizationHelper.GetUmbracoUser(currentUser); + + var result = await _contentPermissionService.AuthorizeDescendantsAccessAsync(user, parentKey, permissionsToCheck); + + return result == ContentAuthorizationStatus.Success; + } + + /// + public async Task IsAuthorizedAtRootLevelAsync(IPrincipal currentUser, ISet permissionsToCheck) + { + IUser user = _authorizationHelper.GetUmbracoUser(currentUser); + + var result = await _contentPermissionService.AuthorizeRootAccessAsync(user, permissionsToCheck); + + return result == ContentAuthorizationStatus.Success; + } + + /// + public async Task IsAuthorizedAtRecycleBinLevelAsync(IPrincipal currentUser, ISet permissionsToCheck) + { + IUser user = _authorizationHelper.GetUmbracoUser(currentUser); + + var result = await _contentPermissionService.AuthorizeBinAccessAsync(user, permissionsToCheck); + + return result == ContentAuthorizationStatus.Success; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionHandler.cs new file mode 100644 index 0000000000..63d098b7f6 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionHandler.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Content; + +/// +/// Authorizes that the current user has the correct permission access to the content item(s) specified in the request. +/// +public class ContentPermissionHandler : MustSatisfyRequirementAuthorizationHandler +{ + private readonly IContentPermissionAuthorizer _contentPermissionAuthorizer; + + /// + /// Initializes a new instance of the class. + /// + /// Authorizer for content access. + public ContentPermissionHandler(IContentPermissionAuthorizer contentPermissionAuthorizer) + => _contentPermissionAuthorizer = contentPermissionAuthorizer; + + /// + protected override async Task IsAuthorized( + AuthorizationHandlerContext context, + ContentPermissionRequirement requirement, + ContentPermissionResource resource) + { + var result = true; + + if (resource.CheckRoot) + { + result &= await _contentPermissionAuthorizer.IsAuthorizedAtRootLevelAsync(context.User, resource.PermissionsToCheck); + } + + if (resource.CheckRecycleBin) + { + result &= await _contentPermissionAuthorizer.IsAuthorizedAtRecycleBinLevelAsync(context.User, resource.PermissionsToCheck); + } + + if (resource.ParentKeyForBranch is not null) + { + result &= await _contentPermissionAuthorizer.IsAuthorizedWithDescendantsAsync(context.User, resource.ParentKeyForBranch.Value, resource.PermissionsToCheck); + } + + if (resource.ContentKeys.Any()) + { + result &= await _contentPermissionAuthorizer.IsAuthorizedAsync(context.User, resource.ContentKeys, resource.PermissionsToCheck); + } + + return result; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionRequirement.cs new file mode 100644 index 0000000000..9398efa27f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionRequirement.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Content; + +/// +/// Authorization requirement for the . +/// +public class ContentPermissionRequirement : IAuthorizationRequirement +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionResource.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionResource.cs new file mode 100644 index 0000000000..16e9cc0dad --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionResource.cs @@ -0,0 +1,147 @@ +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Content; + +/// +/// A resource used for the . +/// +public class ContentPermissionResource : IPermissionResource +{ + /// + /// Creates a with the specified permission and content key or root. + /// + /// The permission to check for. + /// The key of the content or null if root. + /// An instance of . + public static ContentPermissionResource WithKeys(char permissionToCheck, Guid? contentKey) => + contentKey is null + ? Root(permissionToCheck) + : WithKeys(permissionToCheck, contentKey.Value.Yield()); + + /// + /// Creates a with the specified permission and content keys. + /// + /// The permission to check for. + /// The keys of the contents or null if root. + /// An instance of . + public static ContentPermissionResource WithKeys(char permissionToCheck, IEnumerable contentKeys) + { + var hasRoot = contentKeys.Any(x => x is null); + IEnumerable keys = contentKeys.Where(x => x.HasValue).Select(x => x!.Value); + + return new ContentPermissionResource(keys, new HashSet { permissionToCheck }, hasRoot, false, null); + } + + /// + /// Creates a with the specified permission and content key. + /// + /// The permission to check for. + /// The key of the content. + /// An instance of . + public static ContentPermissionResource WithKeys(char permissionToCheck, Guid contentKey) => WithKeys(permissionToCheck, contentKey.Yield()); + + /// + /// Creates a with the specified permission and content keys. + /// + /// The permission to check for. + /// The keys of the contents. + /// An instance of . + public static ContentPermissionResource WithKeys(char permissionToCheck, IEnumerable contentKeys) => + new ContentPermissionResource(contentKeys, new HashSet { permissionToCheck }, false, false, null); + + /// + /// Creates a with the specified permissions and content keys. + /// + /// The permissions to check for. + /// The keys of the contents. + /// An instance of . + public static ContentPermissionResource WithKeys(ISet permissionsToCheck, IEnumerable contentKeys) => + new ContentPermissionResource(contentKeys, permissionsToCheck, false, false, null); + + /// + /// Creates a with the specified permission and the root. + /// + /// The permission to check for. + /// An instance of . + public static ContentPermissionResource Root(char permissionToCheck) => + new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, true, false, null); + + /// + /// Creates a with the specified permissions and the root. + /// + /// The permissions to check for. + /// An instance of . + public static ContentPermissionResource Root(ISet permissionsToCheck) => + new ContentPermissionResource(Enumerable.Empty(), permissionsToCheck, true, false, null); + + /// + /// Creates a with the specified permissions and the recycle bin. + /// + /// The permissions to check for. + /// An instance of . + public static ContentPermissionResource RecycleBin(ISet permissionsToCheck) => + new ContentPermissionResource(Enumerable.Empty(), permissionsToCheck, false, true, null); + + /// + /// Creates a with the specified permission and the recycle bin. + /// + /// The permission to check for. + /// An instance of . + public static ContentPermissionResource RecycleBin(char permissionToCheck) => + new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, false, true, null); + + /// + /// Creates a with the specified permissions and the branch from the specified parent key. + /// + /// The permissions to check for. + /// The parent key of the branch. + /// An instance of . + public static ContentPermissionResource Branch(ISet permissionsToCheck, Guid parentKeyForBranch) => + new ContentPermissionResource(Enumerable.Empty(), permissionsToCheck, false, true, parentKeyForBranch); + + /// + /// Creates a with the specified permission and the branch from the specified parent key. + /// + /// The permission to check for. + /// The parent key of the branch. + /// An instance of . + public static ContentPermissionResource Branch(char permissionToCheck, Guid parentKeyForBranch) => + new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, false, true, parentKeyForBranch); + + private ContentPermissionResource(IEnumerable contentKeys, ISet permissionsToCheck, bool checkRoot, bool checkRecycleBin, Guid? parentKeyForBranch) + { + ContentKeys = contentKeys; + PermissionsToCheck = permissionsToCheck; + CheckRoot = checkRoot; + CheckRecycleBin = checkRecycleBin; + ParentKeyForBranch = parentKeyForBranch; + } + + /// + /// Gets the content keys. + /// + public IEnumerable ContentKeys { get; } + + /// + /// Gets the collection of permissions to authorize. + /// + /// + /// All permissions have to be satisfied when evaluating. + /// + public ISet PermissionsToCheck { get; } + + /// + /// Gets a value indicating whether to check for the root. + /// + public bool CheckRoot { get; } + + /// + /// Gets a value indicating whether to check for the recycle bin. + /// + public bool CheckRecycleBin { get; } + + /// + /// Gets the parent key of a branch. + /// + public Guid? ParentKeyForBranch { get; } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/IContentPermissionAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/IContentPermissionAuthorizer.cs new file mode 100644 index 0000000000..e25a509c80 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/IContentPermissionAuthorizer.cs @@ -0,0 +1,82 @@ +using System.Security.Principal; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Content; + +/// +/// Authorizes content access. +/// +public interface IContentPermissionAuthorizer +{ + /// + /// Authorizes whether the current user has access to the specified content item. + /// + /// The current user's principal. + /// The key of the content item to check for. + /// The permission to authorize. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAsync(IPrincipal currentUser, Guid contentKey, char permissionToCheck) + => IsAuthorizedAsync(currentUser, contentKey.Yield(), new HashSet { permissionToCheck }); + + /// + /// Authorizes whether the current user has access to the specified content item(s). + /// + /// The current user's principal. + /// The keys of the content items to check for. + /// The collection of permissions to authorize. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAsync(IPrincipal currentUser, IEnumerable contentKeys, ISet permissionsToCheck); + + /// + /// Authorizes whether the current user has access to the descendants of the specified content item. + /// + /// The current user's principal. + /// The key of the parent content item. + /// The permission to authorize. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedWithDescendantsAsync(IPrincipal currentUser, Guid parentKey, char permissionToCheck) + => IsAuthorizedWithDescendantsAsync(currentUser, parentKey, new HashSet { permissionToCheck }); + + /// + /// Authorizes whether the current user has access to the descendants of the specified content item. + /// + /// The current user's principal. + /// The key of the parent content item. + /// The collection of permissions to authorize. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedWithDescendantsAsync(IPrincipal currentUser, Guid parentKey, ISet permissionsToCheck); + + /// + /// Authorizes whether the current user has access to the root item. + /// + /// The current user's principal. + /// The permission to authorize. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAtRootLevelAsync(IPrincipal currentUser, char permissionToCheck) + => IsAuthorizedAtRootLevelAsync(currentUser, new HashSet { permissionToCheck }); + + /// + /// Authorizes whether the current user has access to the root item. + /// + /// The current user's principal. + /// The collection of permissions to authorize. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAtRootLevelAsync(IPrincipal currentUser, ISet permissionsToCheck); + + /// + /// Authorizes whether the current user has access to the recycle bin item. + /// + /// The current user's principal. + /// The permission to authorize. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAtRecycleBinLevelAsync(IPrincipal currentUser, char permissionToCheck) + => IsAuthorizedAtRecycleBinLevelAsync(currentUser, new HashSet { permissionToCheck }); + + /// + /// Authorizes whether the current user has access to the recycle bin item. + /// + /// The current user's principal. + /// The collection of permissions to authorize. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAtRecycleBinLevelAsync(IPrincipal currentUser, ISet permissionsToCheck); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs new file mode 100644 index 0000000000..e4969d9960 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginHandler.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.DenyLocalLogin; + +/// +/// Ensures the resource cannot be accessed if +/// returns true. +/// +public class DenyLocalLoginHandler : MustSatisfyRequirementAuthorizationHandler +{ + // private readonly IBackOfficeExternalLoginProviders _externalLogins; + // + // /// + // /// Initializes a new instance of the class. + // /// + // /// Provides access to instances. + // public DenyLocalLoginHandler(IBackOfficeExternalLoginProviders externalLogins) + // => _externalLogins = externalLogins; + // + // /// + // protected override Task IsAuthorized(AuthorizationHandlerContext context, DenyLocalLoginRequirement requirement) + // => Task.FromResult(!_externalLogins.HasDenyLocalLogin()); + + // FIXME: Replace the value of isDenied with above implementation, once we have IBackOfficeExternalLoginProviders and related classes + // moved from Umbraco.Web.Backoffice + // FIXME: Remove [AllowAnonymous] from implementers of and in when we have the proper implementation + protected override Task IsAuthorized(AuthorizationHandlerContext context, DenyLocalLoginRequirement requirement) + { + // Some logic here - for now we will always authorize successfully + var isDenied = false; + + if (isDenied is false) + { + // Now allow anonymous (RequireAuthenticatedUser() adds this requirement) - necessary for some of the endpoints (BackOfficeController.Login()) + var denyAnonymousUserRequirements = context.PendingRequirements.OfType(); + foreach (var denyAnonymousUserRequirement in denyAnonymousUserRequirements) + { + context.Succeed(denyAnonymousUserRequirement); + } + } + + return Task.FromResult(isDenied is false); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginRequirement.cs new file mode 100644 index 0000000000..922c2ca4bc --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/DenyLocalLogin/DenyLocalLoginRequirement.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.DenyLocalLogin; + +/// +/// Marker requirement for the . +/// +public class DenyLocalLoginRequirement : IAuthorizationRequirement +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizeHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizeHandler.cs new file mode 100644 index 0000000000..b66f439802 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizeHandler.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Feature; + +/// +/// Authorizes that the controller is an authorized Umbraco feature. +/// +public class FeatureAuthorizeHandler : MustSatisfyRequirementAuthorizationHandler +{ + private readonly IFeatureAuthorizer _featureAuthorizer; + + /// + /// Initializes a new instance of the class. + /// + /// Authorizer for Umbraco features. + public FeatureAuthorizeHandler(IFeatureAuthorizer featureAuthorizer) + => _featureAuthorizer = featureAuthorizer; + + /// + protected override async Task IsAuthorized(AuthorizationHandlerContext context, FeatureAuthorizeRequirement requirement) + => await _featureAuthorizer.IsAuthorizedAsync(context); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizeRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizeRequirement.cs new file mode 100644 index 0000000000..8c8f29145d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizeRequirement.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Feature; + +/// +/// Authorization requirement for the . +/// +public class FeatureAuthorizeRequirement : IAuthorizationRequirement +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizer.cs new file mode 100644 index 0000000000..25af129346 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/FeatureAuthorizer.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Features; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Feature; + +/// +internal sealed class FeatureAuthorizer : IFeatureAuthorizer +{ + private readonly IRuntimeState _runtimeState; + private readonly UmbracoFeatures _umbracoFeatures; + + public FeatureAuthorizer(IRuntimeState runtimeState, UmbracoFeatures umbracoFeatures) + { + _runtimeState = runtimeState; + _umbracoFeatures = umbracoFeatures; + } + + /// + public async Task IsAuthorizedAsync(AuthorizationHandlerContext context) + { + Endpoint? endpoint = null; + + if (_runtimeState.Level != RuntimeLevel.Run && _runtimeState.Level != RuntimeLevel.Upgrade) + { + return false; + } + + switch (context.Resource) + { + case DefaultHttpContext defaultHttpContext: + { + IEndpointFeature? endpointFeature = defaultHttpContext.Features.Get(); + endpoint = endpointFeature?.Endpoint; + break; + } + + case AuthorizationFilterContext authorizationFilterContext: + { + IEndpointFeature? endpointFeature = + authorizationFilterContext.HttpContext.Features.Get(); + endpoint = endpointFeature?.Endpoint; + break; + } + + case Endpoint resourceEndpoint: + { + endpoint = resourceEndpoint; + break; + } + } + + if (endpoint is null) + { + throw new InvalidOperationException("This authorization handler can only be applied to controllers routed with endpoint routing."); + } + + ControllerActionDescriptor? actionDescriptor = endpoint.Metadata.GetMetadata(); + Type? controllerType = actionDescriptor?.ControllerTypeInfo.AsType(); + return await Task.FromResult(_umbracoFeatures.IsControllerEnabled(controllerType)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/IFeatureAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/IFeatureAuthorizer.cs new file mode 100644 index 0000000000..febfb77054 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Feature/IFeatureAuthorizer.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Feature; + +/// +/// Authorizes Umbraco features. +/// +public interface IFeatureAuthorizer +{ + /// + /// Authorizes the current action. + /// + /// The authorization context. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAsync(AuthorizationHandlerContext context); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/IAuthorizationHelper.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/IAuthorizationHelper.cs new file mode 100644 index 0000000000..b1bb4b9617 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/IAuthorizationHelper.cs @@ -0,0 +1,19 @@ +using System.Security.Principal; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Api.Management.Security.Authorization; + +/// +/// Utility class for working with policy authorizers. +/// +public interface IAuthorizationHelper +{ + /// + /// Converts an into . + /// + /// The current user's principal. + /// + /// . + /// + IUser GetUmbracoUser(IPrincipal currentUser); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/IPermissionResource.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/IPermissionResource.cs new file mode 100644 index 0000000000..ada11db86f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/IPermissionResource.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Api.Management.Security.Authorization; + +public interface IPermissionResource +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/IMediaPermissionAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/IMediaPermissionAuthorizer.cs new file mode 100644 index 0000000000..056b13419e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/IMediaPermissionAuthorizer.cs @@ -0,0 +1,41 @@ +using System.Security.Principal; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Media; + +/// +/// Authorizes media access. +/// +public interface IMediaPermissionAuthorizer +{ + /// + /// Authorizes whether the current user has access to the specified media item. + /// + /// The current user's principal. + /// The key of the media item to check for. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAsync(IPrincipal currentUser, Guid mediaKey) + => IsAuthorizedAsync(currentUser, mediaKey.Yield()); + + /// + /// Authorizes whether the current user has access to the specified media item(s). + /// + /// The current user's principal. + /// The keys of the media items to check for. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAsync(IPrincipal currentUser, IEnumerable mediaKeys); + + /// + /// Authorizes whether the current user has access to the root item. + /// + /// The current user's principal. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAtRootLevelAsync(IPrincipal currentUser); + + /// + /// Authorizes whether the current user has access to the recycle bin item. + /// + /// The current user's principal. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAtRecycleBinLevelAsync(IPrincipal currentUser); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionAuthorizer.cs new file mode 100644 index 0000000000..e538b19d2e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionAuthorizer.cs @@ -0,0 +1,55 @@ +using System.Security.Principal; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.AuthorizationStatus; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Media; + +/// +internal sealed class MediaPermissionAuthorizer : IMediaPermissionAuthorizer +{ + private readonly IAuthorizationHelper _authorizationHelper; + private readonly IMediaPermissionService _mediaPermissionService; + + public MediaPermissionAuthorizer(IAuthorizationHelper authorizationHelper, IMediaPermissionService mediaPermissionService) + { + _authorizationHelper = authorizationHelper; + _mediaPermissionService = mediaPermissionService; + } + + /// + public async Task IsAuthorizedAsync(IPrincipal currentUser, IEnumerable mediaKeys) + { + if (!mediaKeys.Any()) + { + // Must succeed this requirement since we cannot process it. + return true; + } + + IUser user = _authorizationHelper.GetUmbracoUser(currentUser); + + var result = await _mediaPermissionService.AuthorizeAccessAsync(user, mediaKeys); + + return result == MediaAuthorizationStatus.Success; + } + + /// + public async Task IsAuthorizedAtRootLevelAsync(IPrincipal currentUser) + { + IUser user = _authorizationHelper.GetUmbracoUser(currentUser); + + var result = await _mediaPermissionService.AuthorizeRootAccessAsync(user); + + return result == MediaAuthorizationStatus.Success; + } + + /// + public async Task IsAuthorizedAtRecycleBinLevelAsync(IPrincipal currentUser) + { + IUser user = _authorizationHelper.GetUmbracoUser(currentUser); + + var result = await _mediaPermissionService.AuthorizeBinAccessAsync(user); + + return result == MediaAuthorizationStatus.Success; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionHandler.cs new file mode 100644 index 0000000000..e383fddc99 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionHandler.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Media; + +/// +/// Authorizes that the current user has the correct permission access to the media item(s) specified in the request. +/// +public class MediaPermissionHandler : MustSatisfyRequirementAuthorizationHandler +{ + private readonly IMediaPermissionAuthorizer _mediaPermissionAuthorizer; + + /// + /// Initializes a new instance of the class. + /// + /// Authorizer for media access. + public MediaPermissionHandler(IMediaPermissionAuthorizer mediaPermissionAuthorizer) + => _mediaPermissionAuthorizer = mediaPermissionAuthorizer; + + /// + protected override async Task IsAuthorized( + AuthorizationHandlerContext context, + MediaPermissionRequirement requirement, + MediaPermissionResource resource) + { + var result = true; + + if (resource.CheckRoot) + { + result &= await _mediaPermissionAuthorizer.IsAuthorizedAtRootLevelAsync(context.User); + } + + if (resource.CheckRecycleBin) + { + result &= await _mediaPermissionAuthorizer.IsAuthorizedAtRecycleBinLevelAsync(context.User); + } + + if (resource.MediaKeys.Any()) + { + result &= await _mediaPermissionAuthorizer.IsAuthorizedAsync(context.User, resource.MediaKeys); + } + + return result; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionRequirement.cs new file mode 100644 index 0000000000..3cc6fb3002 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionRequirement.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Media; + +/// +/// Authorization requirement for the . +/// +public class MediaPermissionRequirement : IAuthorizationRequirement +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionResource.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionResource.cs new file mode 100644 index 0000000000..52d93fab1b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Media/MediaPermissionResource.cs @@ -0,0 +1,82 @@ +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.Media; + +/// +/// A resource used for the . +/// +public class MediaPermissionResource : IPermissionResource +{ + /// + /// Creates a with the specified key. + /// + /// The key of the media or null if root. + /// An instance of . + public static MediaPermissionResource WithKeys(Guid? mediaKey) => + mediaKey is null + ? Root() + : WithKeys(mediaKey.Value.Yield()); + + /// + /// Creates a with the specified key. + /// + /// The key of the media. + /// An instance of . + public static MediaPermissionResource WithKeys(Guid mediaKey) => WithKeys(mediaKey.Yield()); + + /// + /// Creates a with the specified keys. + /// + /// The keys of the medias or null if root. + /// An instance of . + public static MediaPermissionResource WithKeys(IEnumerable mediaKeys) + { + var hasRoot = mediaKeys.Any(x => x is null); + IEnumerable keys = mediaKeys.Where(x => x.HasValue).Select(x => x!.Value); + return new MediaPermissionResource(keys, hasRoot, false); + } + + /// + /// Creates a with the specified keys. + /// + /// The keys of the medias. + /// An instance of . + public static MediaPermissionResource WithKeys(IEnumerable mediaKeys) => + new MediaPermissionResource(mediaKeys, false, false); + + /// + /// Creates a with the root. + /// + /// An instance of . + public static MediaPermissionResource Root() => + new MediaPermissionResource(Enumerable.Empty(), true, false); + + /// + /// Creates a with the recycle bin. + /// + /// An instance of . + public static MediaPermissionResource RecycleBin() => + new MediaPermissionResource(Enumerable.Empty(), false, true); + + private MediaPermissionResource(IEnumerable mediaKeys, bool checkRoot, bool checkRecycleBin) + { + MediaKeys = mediaKeys; + CheckRoot = checkRoot; + CheckRecycleBin = checkRecycleBin; + } + + /// + /// Gets the media keys. + /// + public IEnumerable MediaKeys { get; } + + /// + /// Gets a value indicating whether to check the root. + /// + public bool CheckRoot { get; } + + /// + /// Gets a value indicating whether to check the recycle bin. + /// + public bool CheckRecycleBin { get; } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/MustSatisfyRequirementAuthorizationHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/MustSatisfyRequirementAuthorizationHandler.cs new file mode 100644 index 0000000000..f1983dc351 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/MustSatisfyRequirementAuthorizationHandler.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization; + +/// +/// Abstract handler that must satisfy the requirement so Succeed or Fail will be called no matter what. +/// +/// Authorization requirement. +/// +/// ASP.NET Core Authorization handlers are not required to satisfy the requirement and generally don't explicitly call context.Fail +/// when the requirement isn't satisfied (as other handlers for the same requirement may succeed), however in many simple cases +/// explicitly calling Succeed or Fail is what we want which is what this class is used for. +/// +public abstract class MustSatisfyRequirementAuthorizationHandler : AuthorizationHandler + where T : IAuthorizationRequirement +{ + /// + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement) + { + var isAuth = await IsAuthorized(context, requirement); + if (isAuth) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + } + + /// + /// Returns true if the requirement is succeeded or ignored, + /// returns false if the requirement is explicitly not met. + /// + /// The authorization context. + /// The authorization requirement. + /// True if request is authorized, false if not. + protected abstract Task IsAuthorized(AuthorizationHandlerContext context, T requirement); +} + +/// +/// Abstract handler that must satisfy the requirement so Succeed or Fail will be called no matter what. +/// +/// Authorization requirement. +/// Resource to authorize access to. +/// +/// ASP.NET Core Authorization handlers are not required to satisfy the requirement and generally don't explicitly call context.Fail +/// when the requirement isn't satisfied (as other handlers for the same requirement may succeed), however in many simple cases +/// explicitly calling Succeed or Fail is what we want which is what this class is used for. +/// +public abstract class MustSatisfyRequirementAuthorizationHandler : AuthorizationHandler + where T : IAuthorizationRequirement +{ + /// + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement, + TResource resource) + { + var isAuth = await IsAuthorized(context, requirement, resource); + if (isAuth) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + } + + /// + /// Returns true if the requirement is succeeded or ignored, + /// returns false if the requirement is explicitly not met. + /// + /// The authorization context. + /// The authorization requirement. + /// The resource to authorize access to. + /// True if request is authorized, false if not. + protected abstract Task IsAuthorized(AuthorizationHandlerContext context, T requirement, TResource resource); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/IUserPermissionAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/IUserPermissionAuthorizer.cs new file mode 100644 index 0000000000..e126f306c8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/IUserPermissionAuthorizer.cs @@ -0,0 +1,27 @@ +using System.Security.Principal; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorizes user access. +/// +public interface IUserPermissionAuthorizer +{ + /// + /// Authorizes whether the current user has access to the specified user account. + /// + /// The current user's principal. + /// The key of the user to check for. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAsync(IPrincipal currentUser, Guid userKey) + => IsAuthorizedAsync(currentUser, userKey.Yield()); + + /// + /// Authorizes whether the current user has access to the specified user account(s). + /// + /// The current user's principal. + /// The keys of the users to check for. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAsync(IPrincipal currentUser, IEnumerable userKeys); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionAuthorizer.cs new file mode 100644 index 0000000000..f304f4effd --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionAuthorizer.cs @@ -0,0 +1,35 @@ +using System.Security.Principal; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.AuthorizationStatus; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +internal sealed class UserPermissionAuthorizer : IUserPermissionAuthorizer +{ + private readonly IAuthorizationHelper _authorizationHelper; + private readonly IUserPermissionService _userPermissionService; + + public UserPermissionAuthorizer(IAuthorizationHelper authorizationHelper, IUserPermissionService userPermissionService) + { + _authorizationHelper = authorizationHelper; + _userPermissionService = userPermissionService; + } + + /// + public async Task IsAuthorizedAsync(IPrincipal currentUser, IEnumerable userKeys) + { + if (!userKeys.Any()) + { + // Must succeed this requirement since we cannot process it. + return true; + } + + IUser performingUser = _authorizationHelper.GetUmbracoUser(currentUser); + + var result = await _userPermissionService.AuthorizeAccessAsync(performingUser, userKeys); + + return result == UserAuthorizationStatus.Success; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionHandler.cs new file mode 100644 index 0000000000..7330eb59b7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionHandler.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorizes that the current user has the correct permission access to perform actions on the user account(s) specified in the request. +/// +public class UserPermissionHandler : MustSatisfyRequirementAuthorizationHandler +{ + private readonly IUserPermissionAuthorizer _userPermissionAuthorizer; + + /// + /// Initializes a new instance of the class. + /// + /// Authorizer for user access. + public UserPermissionHandler(IUserPermissionAuthorizer userPermissionAuthorizer) + => _userPermissionAuthorizer = userPermissionAuthorizer; + + /// + protected override async Task IsAuthorized( + AuthorizationHandlerContext context, + UserPermissionRequirement requirement, + UserPermissionResource resource) => + await _userPermissionAuthorizer.IsAuthorizedAsync(context.User, resource.UserKeys); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionRequirement.cs new file mode 100644 index 0000000000..1f76c59fd6 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionRequirement.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorization requirement for the . +/// +public class UserPermissionRequirement : IAuthorizationRequirement +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionResource.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionResource.cs new file mode 100644 index 0000000000..d004908c86 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/UserPermissionResource.cs @@ -0,0 +1,34 @@ +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// A resource used for the . +/// +public class UserPermissionResource : IPermissionResource +{ + /// + /// Creates a with the specified keys. + /// + /// The key of the user. + /// An instance of . + public static UserPermissionResource WithKeys(Guid userKey) => WithKeys(userKey.Yield()); + + /// + /// Creates a with the specified keys. + /// + /// The keys of the users. + /// An instance of . + public static UserPermissionResource WithKeys(IEnumerable userKeys) => new(userKeys); + + /// + /// Initializes a new instance of the class. + /// + /// The keys of the users. + private UserPermissionResource(IEnumerable userKeys) => UserKeys = userKeys; + + /// + /// Gets the user keys. + /// + public IEnumerable UserKeys { get; } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/IUserGroupPermissionAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/IUserGroupPermissionAuthorizer.cs new file mode 100644 index 0000000000..47b7d4c570 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/IUserGroupPermissionAuthorizer.cs @@ -0,0 +1,27 @@ +using System.Security.Principal; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.UserGroup; + +/// +/// Authorizes user group access. +/// +public interface IUserGroupPermissionAuthorizer +{ + /// + /// Authorizes whether the current user has access to the specified user group. + /// + /// The current user's principal. + /// The key of the user group to check against. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAsync(IPrincipal currentUser, Guid userGroupKey) + => IsAuthorizedAsync(currentUser, userGroupKey.Yield()); + + /// + /// Authorizes whether the current user has access to the specified user group(s). + /// + /// The current user's principal. + /// The keys of the user groups to check against. + /// Returns true if authorization is successful, otherwise false. + Task IsAuthorizedAsync(IPrincipal currentUser, IEnumerable userGroupKeys); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionAuthorizer.cs new file mode 100644 index 0000000000..eaa9b97b0f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionAuthorizer.cs @@ -0,0 +1,35 @@ +using System.Security.Principal; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.AuthorizationStatus; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.UserGroup; + +/// +internal sealed class UserGroupPermissionAuthorizer : IUserGroupPermissionAuthorizer +{ + private readonly IAuthorizationHelper _authorizationHelper; + private readonly IUserGroupPermissionService _userGroupPermissionService; + + public UserGroupPermissionAuthorizer(IAuthorizationHelper authorizationHelper, IUserGroupPermissionService userGroupPermissionService) + { + _authorizationHelper = authorizationHelper; + _userGroupPermissionService = userGroupPermissionService; + } + + /// + public async Task IsAuthorizedAsync(IPrincipal currentUser, IEnumerable userGroupKeys) + { + if (!userGroupKeys.Any()) + { + // Must succeed this requirement since we cannot process it. + return true; + } + + IUser user = _authorizationHelper.GetUmbracoUser(currentUser); + + var result = await _userGroupPermissionService.AuthorizeAccessAsync(user, userGroupKeys); + + return result == UserGroupAuthorizationStatus.Success; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionHandler.cs new file mode 100644 index 0000000000..a53d930f82 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionHandler.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.UserGroup; + +/// +/// Authorizes that the current user has access to the user group(s) specified in the request. +/// +public class UserGroupPermissionHandler : MustSatisfyRequirementAuthorizationHandler +{ + private readonly IUserGroupPermissionAuthorizer _userGroupPermissionAuthorizer; + + /// + /// Initializes a new instance of the class. + /// + /// Authorizer for user group access. + public UserGroupPermissionHandler(IUserGroupPermissionAuthorizer userGroupPermissionAuthorizer) + => _userGroupPermissionAuthorizer = userGroupPermissionAuthorizer; + + /// + protected override async Task IsAuthorized( + AuthorizationHandlerContext context, + UserGroupPermissionRequirement requirement, + UserGroupPermissionResource resource) => + await _userGroupPermissionAuthorizer.IsAuthorizedAsync(context.User, resource.UserGroupKeys); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionRequirement.cs new file mode 100644 index 0000000000..4924bf07a8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionRequirement.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.UserGroup; + +/// +/// Authorization requirement for the . +/// +public class UserGroupPermissionRequirement : IAuthorizationRequirement +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionResource.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionResource.cs new file mode 100644 index 0000000000..e8ea6ad467 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/UserGroup/UserGroupPermissionResource.cs @@ -0,0 +1,34 @@ +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.UserGroup; + +/// +/// A resource used for the . +/// +public class UserGroupPermissionResource : IPermissionResource +{ + /// + /// Creates a with the specified key. + /// + /// The key of the user group. + /// An instance of . + public static UserGroupPermissionResource WithKeys(Guid userGroupKey) => WithKeys(userGroupKey.Yield()); + + /// + /// Creates a with the specified keys. + /// + /// The keys of the user groups. + /// An instance of . + public static UserGroupPermissionResource WithKeys(IEnumerable userGroupKeys) => new(userGroupKeys); + + /// + /// Initializes a new instance of the class. + /// + /// The keys of the user groups. + public UserGroupPermissionResource(IEnumerable userGroupKeys) => UserGroupKeys = userGroupKeys; + + /// + /// Gets the user group keys. + /// + public IEnumerable UserGroupKeys { get; } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs index 3fbd3115eb..b23e1542d1 100644 --- a/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs @@ -42,24 +42,7 @@ public class BackOfficeApplicationManager : OpenIdDictApplicationManagerBase, IB } await CreateOrUpdate( - new OpenIddictApplicationDescriptor - { - DisplayName = "Umbraco back-office access", - ClientId = Constants.OAuthClientIds.BackOffice, - RedirectUris = - { - CallbackUrlFor(_backOfficeHost ?? backOfficeUrl, _authorizeCallbackPathName ?? "/umbraco") - }, - Type = OpenIddictConstants.ClientTypes.Public, - Permissions = - { - OpenIddictConstants.Permissions.Endpoints.Authorization, - OpenIddictConstants.Permissions.Endpoints.Token, - OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, - OpenIddictConstants.Permissions.GrantTypes.RefreshToken, - OpenIddictConstants.Permissions.ResponseTypes.Code - } - }, + BackofficeOpenIddictApplicationDescriptor(backOfficeUrl), cancellationToken); if (_webHostEnvironment.IsProduction()) @@ -111,5 +94,25 @@ public class BackOfficeApplicationManager : OpenIdDictApplicationManagerBase, IB } } + public OpenIddictApplicationDescriptor BackofficeOpenIddictApplicationDescriptor(Uri backOfficeUrl) => + new() + { + DisplayName = "Umbraco back-office access", + ClientId = Constants.OAuthClientIds.BackOffice, + RedirectUris = + { + CallbackUrlFor(_backOfficeHost ?? backOfficeUrl, _authorizeCallbackPathName ?? "/umbraco") + }, + Type = OpenIddictConstants.ClientTypes.Public, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, + OpenIddictConstants.Permissions.ResponseTypes.Code + } + }; + private static Uri CallbackUrlFor(Uri url, string relativePath) => new Uri( $"{url.GetLeftPart(UriPartial.Authority)}/{relativePath.TrimStart(Constants.CharArrays.ForwardSlash)}"); } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index f47d813839..1ca693ae19 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -282,6 +282,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); @@ -299,12 +300,14 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs index a604b3e017..5d33108a0f 100644 --- a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs @@ -61,6 +61,31 @@ public static class ClaimsIdentityExtensions return userId; } + /// + /// Returns the user key from the of the claim type "sub". + /// + /// + /// + /// The string value of the user id if found otherwise null. + /// + public static Guid? GetUserKey(this IIdentity identity) + { + if (identity is null) + { + throw new ArgumentNullException(nameof(identity)); + } + + string? userKey = null; + if (identity is ClaimsIdentity claimsIdentity) + { + userKey = claimsIdentity.FindFirstValue("sub"); + } + + return Guid.TryParse(userKey, out Guid result) + ? result + : null; + } + /// /// Returns the user name from the of either the claim type or /// "preferred_username" diff --git a/src/Umbraco.Core/Security/ContentPermissions.cs b/src/Umbraco.Core/Security/ContentPermissions.cs index 6c70326699..bdd6b5179d 100644 --- a/src/Umbraco.Core/Security/ContentPermissions.cs +++ b/src/Umbraco.Core/Security/ContentPermissions.cs @@ -70,11 +70,13 @@ public class ContentPermissions formattedPath.Contains(string.Concat(",", x.ToString(CultureInfo.InvariantCulture), ","))); } + [Obsolete($"Please use {nameof(IContentPermissionService)} instead, scheduled for removal in V15.")] public ContentAccess CheckPermissions( IContent content, IUser user, char permissionToCheck) => CheckPermissions(content, user, new[] { permissionToCheck }); + [Obsolete($"Please use {nameof(IContentPermissionService)} instead, scheduled for removal in V15.")] public ContentAccess CheckPermissions( IContent? content, IUser? user, @@ -108,11 +110,13 @@ public class ContentPermissions : ContentAccess.Denied; } + [Obsolete($"Please use {nameof(IContentPermissionService)} instead, scheduled for removal in V15.")] public ContentAccess CheckPermissions( IUmbracoEntity entity, IUser? user, char permissionToCheck) => CheckPermissions(entity, user, new[] { permissionToCheck }); + [Obsolete($"Please use {nameof(IContentPermissionService)} instead, scheduled for removal in V15.")] public ContentAccess CheckPermissions( IUmbracoEntity entity, IUser? user, @@ -154,6 +158,7 @@ public class ContentPermissions /// The item resolved if one was found for the id /// /// + [Obsolete($"Please use {nameof(IContentPermissionService)} instead, scheduled for removal in V15.")] public ContentAccess CheckPermissions( int nodeId, IUser user, @@ -199,7 +204,7 @@ public class ContentPermissions } // get the implicit/inherited permissions for the user for this path - // if there is no entity for this id, than just use the id as the path (i.e. -1 or -20) + // if there is no entity for this id, then just use the id as the path (i.e. -1 or -20) return CheckPermissionsPath(entity?.Path ?? nodeId.ToString(), user, permissionsToCheck) ? ContentAccess.Granted : ContentAccess.Denied; @@ -213,6 +218,7 @@ public class ContentPermissions /// The item resolved if one was found for the id /// /// + [Obsolete($"Please use {nameof(IContentPermissionService)} instead, scheduled for removal in V15.")] public ContentAccess CheckPermissions( int nodeId, IUser? user, @@ -258,12 +264,13 @@ public class ContentPermissions } // 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) + // if there is no content item for this id, then just use the id as the path (i.e. -1 or -20) return CheckPermissionsPath(contentItem?.Path ?? nodeId.ToString(), user, permissionsToCheck) ? ContentAccess.Granted : ContentAccess.Denied; } + [Obsolete($"Please use {nameof(IContentPermissionService)} instead, scheduled for removal in V15.")] private bool CheckPermissionsPath(string? path, IUser user, IReadOnlyList? permissionsToCheck = null) { if (permissionsToCheck == null) diff --git a/src/Umbraco.Core/Security/MediaPermissions.cs b/src/Umbraco.Core/Security/MediaPermissions.cs index c46d32f565..6a79b91b35 100644 --- a/src/Umbraco.Core/Security/MediaPermissions.cs +++ b/src/Umbraco.Core/Security/MediaPermissions.cs @@ -8,6 +8,7 @@ namespace Umbraco.Cms.Core.Security; /// /// Checks user access to media /// +[Obsolete($"Please use {nameof(IMediaPermissionService)} instead, scheduled for removal in V15.")] public class MediaPermissions { private readonly AppCaches _appCaches; diff --git a/src/Umbraco.Core/Services/AuditService.cs b/src/Umbraco.Core/Services/AuditService.cs index 89f939a3cd..7da7bf7d9c 100644 --- a/src/Umbraco.Core/Services/AuditService.cs +++ b/src/Umbraco.Core/Services/AuditService.cs @@ -235,13 +235,13 @@ public sealed class AuditService : RepositoryService, IAuditService using (ScopeProvider.CreateCoreScope(autoComplete: true)) { - IEntitySlim? entity = _entityRepository.Get(entityKey); - if (entity is null) + var user = await _userService.GetAsync(entityKey); + if (user is null) { throw new ArgumentNullException($"Could not find user with key {entityKey}"); } - IQuery query = Query().Where(x => x.Id == entity.Id); + IQuery query = Query().Where(x => x.UserId == user.Id); IQuery? customFilter = sinceDate.HasValue ? Query().Where(x => x.CreateDate >= sinceDate) : null; PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); diff --git a/src/Umbraco.Core/Services/AuthorizationStatus/ContentAuthorizationStatus.cs b/src/Umbraco.Core/Services/AuthorizationStatus/ContentAuthorizationStatus.cs new file mode 100644 index 0000000000..e0408d4ba4 --- /dev/null +++ b/src/Umbraco.Core/Services/AuthorizationStatus/ContentAuthorizationStatus.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core.Services.AuthorizationStatus; + +public enum ContentAuthorizationStatus +{ + Success, + NotFound, + UnauthorizedMissingBinAccess, + UnauthorizedMissingDescendantAccess, + UnauthorizedMissingPathAccess, + UnauthorizedMissingRootAccess +} diff --git a/src/Umbraco.Core/Services/AuthorizationStatus/MediaAuthorizationStatus.cs b/src/Umbraco.Core/Services/AuthorizationStatus/MediaAuthorizationStatus.cs new file mode 100644 index 0000000000..191c5e2850 --- /dev/null +++ b/src/Umbraco.Core/Services/AuthorizationStatus/MediaAuthorizationStatus.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Services.AuthorizationStatus; + +public enum MediaAuthorizationStatus +{ + Success, + NotFound, + UnauthorizedMissingBinAccess, + UnauthorizedMissingPathAccess, + UnauthorizedMissingRootAccess +} diff --git a/src/Umbraco.Core/Services/AuthorizationStatus/UserAuthorizationStatus.cs b/src/Umbraco.Core/Services/AuthorizationStatus/UserAuthorizationStatus.cs new file mode 100644 index 0000000000..ec49560fcf --- /dev/null +++ b/src/Umbraco.Core/Services/AuthorizationStatus/UserAuthorizationStatus.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Services.AuthorizationStatus; + +public enum UserAuthorizationStatus +{ + Success, + UnauthorizedMissingAdminAccess +} diff --git a/src/Umbraco.Core/Services/ContentPermissionService.cs b/src/Umbraco.Core/Services/ContentPermissionService.cs new file mode 100644 index 0000000000..aafcddf8b8 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentPermissionService.cs @@ -0,0 +1,158 @@ +using System.Globalization; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.AuthorizationStatus; + +namespace Umbraco.Cms.Core.Services; + +/// +internal sealed class ContentPermissionService : IContentPermissionService +{ + private readonly IContentService _contentService; + private readonly IEntityService _entityService; + private readonly IUserService _userService; + private readonly AppCaches _appCaches; + + public ContentPermissionService( + IContentService contentService, + IEntityService entityService, + IUserService userService, + AppCaches appCaches) + { + _contentService = contentService; + _entityService = entityService; + _userService = userService; + _appCaches = appCaches; + } + + /// + public async Task AuthorizeAccessAsync( + IUser user, + IEnumerable contentKeys, + ISet permissionsToCheck) + { + var contentItems = _contentService.GetByIds(contentKeys).ToArray(); + + if (contentItems.Length == 0) + { + return ContentAuthorizationStatus.NotFound; + } + + if (contentItems.Any(contentItem => user.HasPathAccess(contentItem, _entityService, _appCaches) == false)) + { + return ContentAuthorizationStatus.UnauthorizedMissingPathAccess; + } + + return HasPermissionAccess(user, contentItems.Select(c => c.Path), permissionsToCheck) + ? ContentAuthorizationStatus.Success + : ContentAuthorizationStatus.UnauthorizedMissingPathAccess; + } + + /// + public async Task AuthorizeDescendantsAccessAsync( + IUser user, + Guid parentKey, + ISet permissionsToCheck) + { + var denied = new List(); + var page = 0; + const int pageSize = 500; + var total = long.MaxValue; + + IContent? contentItem = _contentService.GetById(parentKey); + + if (contentItem is null) + { + return ContentAuthorizationStatus.NotFound; + } + + while (page * pageSize < total) + { + // Order descendents 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. + IEnumerable descendants = _entityService.GetPagedDescendants( + contentItem.Id, + UmbracoObjectTypes.Document, + page++, + pageSize, + out total, + ordering: Ordering.By("path")); + + foreach (IEntitySlim descendant in descendants) + { + var hasPathAccess = user.HasContentPathAccess(descendant, _entityService, _appCaches); + var hasPermissionAccess = HasPermissionAccess(user, new[] { descendant.Path }, permissionsToCheck); + + // 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 => descendant.Path.StartsWith($"{x.Path},")) || hasPathAccess == false || hasPermissionAccess == false) + { + denied.Add(descendant); + } + } + } + + return denied.Count == 0 + ? ContentAuthorizationStatus.Success + : ContentAuthorizationStatus.UnauthorizedMissingDescendantAccess; + } + + /// + public async Task AuthorizeRootAccessAsync(IUser user, ISet permissionsToCheck) + { + var hasAccess = user.HasContentRootAccess(_entityService, _appCaches); + + if (hasAccess == false) + { + return ContentAuthorizationStatus.UnauthorizedMissingRootAccess; + } + + // In this case, we have to use the Root id as path (i.e. -1) since we don't have a content item + return HasPermissionAccess(user, new[] { Constants.System.RootString }, permissionsToCheck) + ? ContentAuthorizationStatus.Success + : ContentAuthorizationStatus.UnauthorizedMissingPathAccess; + } + + /// + public async Task AuthorizeBinAccessAsync(IUser user, ISet permissionsToCheck) + { + var hasAccess = user.HasContentBinAccess(_entityService, _appCaches); + + if (hasAccess == false) + { + return ContentAuthorizationStatus.UnauthorizedMissingBinAccess; + } + + // In this case, we have to use the Recycle Bin id as path (i.e. -20) since we don't have a content item + return HasPermissionAccess(user, new[] { Constants.System.RecycleBinContentString }, permissionsToCheck) + ? ContentAuthorizationStatus.Success + : ContentAuthorizationStatus.UnauthorizedMissingPathAccess; + } + + /// + /// Check the implicit/inherited permissions of a user for given content items. + /// + /// to check for access. + /// The paths of the content items to check for access. + /// The permissions to authorize. + /// true if the user has the required permissions; otherwise, false. + private bool HasPermissionAccess(IUser user, IEnumerable contentPaths, IEnumerable permissionsToCheck) + { + foreach (var path in contentPaths) + { + // get the implicit/inherited permissions for the user for this path + EntityPermissionSet permissionSet = _userService.GetPermissionsForPath(user, path); + + foreach (var p in permissionsToCheck) + { + if (permissionSet.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + return false; + } + } + } + + return true; + } +} diff --git a/src/Umbraco.Core/Services/IContentPermissionService.cs b/src/Umbraco.Core/Services/IContentPermissionService.cs new file mode 100644 index 0000000000..7ea0b52863 --- /dev/null +++ b/src/Umbraco.Core/Services/IContentPermissionService.cs @@ -0,0 +1,83 @@ +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.AuthorizationStatus; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services; + +/// +/// Manages permissions for content access. +/// +public interface IContentPermissionService +{ + /// + /// Authorize that a user has access to a content item. + /// + /// to authorize. + /// The identifier of the content item to check for access. + /// The permission to authorize. + /// A task resolving into a . + Task AuthorizeAccessAsync(IUser user, Guid contentKey, char permissionToCheck) + => AuthorizeAccessAsync(user, contentKey.Yield(), new HashSet { permissionToCheck }); + + /// + /// Authorize that a user has access to content items. + /// + /// to authorize. + /// The identifiers of the content items to check for access. + /// The collection of permissions to authorize. + /// A task resolving into a . + Task AuthorizeAccessAsync(IUser user, IEnumerable contentKeys, ISet permissionsToCheck); + + /// + /// Authorize that a user has access to the descendant items of a content item. + /// + /// to authorize. + /// The identifier of the parent content item to check its descendants for access. + /// The permission to authorize. + /// A task resolving into a . + Task AuthorizeDescendantsAccessAsync(IUser user, Guid parentKey, char permissionToCheck) + => AuthorizeDescendantsAccessAsync(user, parentKey, new HashSet { permissionToCheck }); + + /// + /// Authorize that a user has access to the descendant items of a content item. + /// + /// to authorize. + /// The identifier of the parent content item to check its descendants for access. + /// The collection of permissions to authorize. + /// A task resolving into a . + Task AuthorizeDescendantsAccessAsync(IUser user, Guid parentKey, ISet permissionsToCheck); + + /// + /// Authorize that a user is allowed to perform action on the content root item. + /// + /// to authorize. + /// The permission to authorize. + /// A task resolving into a . + Task AuthorizeRootAccessAsync(IUser user, char permissionToCheck) + => AuthorizeRootAccessAsync(user, new HashSet { permissionToCheck }); + + /// + /// Authorize that a user is allowed to perform actions on the content root item. + /// + /// to authorize. + /// The collection of permissions to authorize. + /// A task resolving into a . + Task AuthorizeRootAccessAsync(IUser user, ISet permissionsToCheck); + + /// + /// Authorize that a user is allowed to perform action on the content bin item. + /// + /// to authorize. + /// The permission to authorize. + /// A task resolving into a . + Task AuthorizeBinAccessAsync(IUser user, char permissionToCheck) + => AuthorizeBinAccessAsync(user, new HashSet { permissionToCheck }); + + /// + /// Authorize that a user is allowed to perform actions on the content bin item. + /// + /// to authorize. + /// The collection of permissions to authorize. + /// A task resolving into a . + Task AuthorizeBinAccessAsync(IUser user, ISet permissionsToCheck); +} diff --git a/src/Umbraco.Core/Services/IMediaPermissionService.cs b/src/Umbraco.Core/Services/IMediaPermissionService.cs new file mode 100644 index 0000000000..00790b4c5d --- /dev/null +++ b/src/Umbraco.Core/Services/IMediaPermissionService.cs @@ -0,0 +1,42 @@ +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.AuthorizationStatus; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services; + +/// +/// Manages permissions for media access. +/// +public interface IMediaPermissionService +{ + /// + /// Authorize that a user has access to a media item. + /// + /// to authorize. + /// The identifier of the media item to check for access. + /// A task resolving into a . + Task AuthorizeAccessAsync(IUser user, Guid mediaKey) + => AuthorizeAccessAsync(user, mediaKey.Yield()); + + /// + /// Authorize that a user has access to media items. + /// + /// to authorize. + /// The identifiers of the media items to check for access. + /// A task resolving into a . + Task AuthorizeAccessAsync(IUser user, IEnumerable mediaKeys); + + /// + /// Authorize that a user has access to the media root item. + /// + /// to authorize. + /// A task resolving into a . + Task AuthorizeRootAccessAsync(IUser user); + + /// + /// Authorize that a user has access to the media bin item. + /// + /// to authorize. + /// A task resolving into a . + Task AuthorizeBinAccessAsync(IUser user); +} diff --git a/src/Umbraco.Core/Services/IUserGroupPermissionService.cs b/src/Umbraco.Core/Services/IUserGroupPermissionService.cs index 9129dbd965..cb3e4dc5ba 100644 --- a/src/Umbraco.Core/Services/IUserGroupPermissionService.cs +++ b/src/Umbraco.Core/Services/IUserGroupPermissionService.cs @@ -1,5 +1,6 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services.AuthorizationStatus; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; @@ -15,7 +16,7 @@ public interface IUserGroupPermissionService /// The identifier of the user group to check for access. /// A task resolving into a . Task AuthorizeAccessAsync(IUser user, Guid userGroupKey) - => AuthorizeAccessAsync(user, new[] { userGroupKey }); + => AuthorizeAccessAsync(user, userGroupKey.Yield()); /// /// Authorize that a user belongs to user groups. diff --git a/src/Umbraco.Core/Services/IUserGroupService.cs b/src/Umbraco.Core/Services/IUserGroupService.cs index 81142b828d..e6dac4752d 100644 --- a/src/Umbraco.Core/Services/IUserGroupService.cs +++ b/src/Umbraco.Core/Services/IUserGroupService.cs @@ -1,6 +1,6 @@ -using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; @@ -85,7 +85,8 @@ public interface IUserGroupService /// The keys of the user groups to delete. /// An attempt indicating if the operation was a success as well as a more detailed . Task> DeleteAsync(ISet userGroupKeys); - Task> DeleteAsync(Guid userGroupKey) => DeleteAsync(new HashSet(){userGroupKey}); + + Task> DeleteAsync(Guid userGroupKey) => DeleteAsync(new HashSet { userGroupKey }); /// /// Updates the users to have the groups specified. diff --git a/src/Umbraco.Core/Services/IUserPermissionService.cs b/src/Umbraco.Core/Services/IUserPermissionService.cs new file mode 100644 index 0000000000..880608bf16 --- /dev/null +++ b/src/Umbraco.Core/Services/IUserPermissionService.cs @@ -0,0 +1,28 @@ +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.AuthorizationStatus; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services; + +/// +/// Manages permissions for user access. +/// +public interface IUserPermissionService +{ + /// + /// Authorize that a user has access to user account. + /// + /// to authorize. + /// The identifier of the user account to check for access. + /// A task resolving into a . + Task AuthorizeAccessAsync(IUser user, Guid userKey) + => AuthorizeAccessAsync(user, userKey.Yield()); + + /// + /// Authorize that a user has access to user accounts. + /// + /// to authorize. + /// The identifiers of the user accounts to check for access. + /// A task resolving into a . + Task AuthorizeAccessAsync(IUser user, IEnumerable userKeys); +} diff --git a/src/Umbraco.Core/Services/MediaPermissionService.cs b/src/Umbraco.Core/Services/MediaPermissionService.cs new file mode 100644 index 0000000000..54c2a4941d --- /dev/null +++ b/src/Umbraco.Core/Services/MediaPermissionService.cs @@ -0,0 +1,56 @@ +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.AuthorizationStatus; + +namespace Umbraco.Cms.Core.Services; + +/// +internal sealed class MediaPermissionService : IMediaPermissionService +{ + private readonly IMediaService _mediaService; + private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; + + public MediaPermissionService( + IMediaService mediaService, + IEntityService entityService, + AppCaches appCaches) + { + _mediaService = mediaService; + _entityService = entityService; + _appCaches = appCaches; + } + + /// + public async Task AuthorizeAccessAsync(IUser user, IEnumerable mediaKeys) + { + foreach (Guid mediaKey in mediaKeys) + { + IMedia? media = _mediaService.GetById(mediaKey); + if (media is null) + { + return MediaAuthorizationStatus.NotFound; + } + + if (user.HasPathAccess(media, _entityService, _appCaches) == false) + { + return MediaAuthorizationStatus.UnauthorizedMissingPathAccess; + } + } + + return MediaAuthorizationStatus.Success; + } + + /// + public async Task AuthorizeRootAccessAsync(IUser user) + => user.HasMediaRootAccess(_entityService, _appCaches) + ? MediaAuthorizationStatus.Success + : MediaAuthorizationStatus.UnauthorizedMissingRootAccess; + + /// + public async Task AuthorizeBinAccessAsync(IUser user) + => user.HasMediaBinAccess(_entityService, _appCaches) + ? MediaAuthorizationStatus.Success + : MediaAuthorizationStatus.UnauthorizedMissingBinAccess; +} diff --git a/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs index 1a8059a536..97e9b631ad 100644 --- a/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs @@ -1,5 +1,6 @@ namespace Umbraco.Cms.Core.Services.OperationStatus; +// FIXME: Move all authorization statuses to public enum UserGroupOperationStatus { Success, diff --git a/src/Umbraco.Core/Services/UserPermissionService.cs b/src/Umbraco.Core/Services/UserPermissionService.cs new file mode 100644 index 0000000000..a03624fbc2 --- /dev/null +++ b/src/Umbraco.Core/Services/UserPermissionService.cs @@ -0,0 +1,31 @@ +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.AuthorizationStatus; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services; + +/// +internal sealed class UserPermissionService : IUserPermissionService +{ + private readonly IUserService _userService; + + public UserPermissionService(IUserService userService) + => _userService = userService; + + /// + public async Task AuthorizeAccessAsync(IUser user, IEnumerable userKeys) + { + var currentIsAdmin = user.IsAdmin(); + + if (currentIsAdmin) + { + return UserAuthorizationStatus.Success; + } + + var usersToCheck = await _userService.GetAsync(userKeys); + + return usersToCheck.Any(u => u.IsAdmin()) + ? UserAuthorizationStatus.UnauthorizedMissingAdminAccess + : UserAuthorizationStatus.Success; + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeRequirement.cs index 1512d48ad9..d0d5ae1913 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeRequirement.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Authorization; namespace Umbraco.Cms.Web.BackOffice.Authorization; /// -/// Authorization requirement for the +/// Authorization requirement for the . /// public class BackOfficeRequirement : IAuthorizationRequirement { diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs b/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs new file mode 100644 index 0000000000..1eab5885f2 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs @@ -0,0 +1,143 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Net.Mime; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json.Serialization; +using System.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using OpenIddict.Abstractions; +using Umbraco.Cms.Api.Management.Controllers; +using Umbraco.Cms.Api.Management.Controllers.Security; +using Umbraco.Cms.Api.Management.Security; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Security; +using Umbraco.Cms.Tests.Integration.TestServerTest; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi; + +[TestFixture] +public abstract class ManagementApiTest : UmbracoTestServerTestBase + where T : ManagementApiControllerBase +{ + [SetUp] + public async Task Setup() + { + Client.DefaultRequestHeaders + .Accept + .Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + } + + protected override void CustomTestAuthSetup(IServiceCollection services) + { + // We do not wanna fake anything, and thereby have protection + } + + protected abstract Expression> MethodSelector { get; } + + protected virtual string Url => GetManagementApiUrl(MethodSelector); + + protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, bool isAdmin) + { + Guid userKey = Constants.Security.SuperUserKey; + OpenIddictApplicationDescriptor backofficeOpenIddictApplicationDescriptor; + var scopeProvider = GetRequiredService(); + using (var scope = scopeProvider.CreateCoreScope()) + { + var userService = GetRequiredService(); + using var serviceScope = GetRequiredService().CreateScope(); + var userManager = serviceScope.ServiceProvider.GetRequiredService(); + + IUser user; + if (isAdmin) + { + user = await userService.GetAsync(userKey) ?? + throw new Exception("Super user not found."); + user.Username = user.Email = username; + userService.Save(user); + } + else + { + user = (await userService.CreateAsync(Constants.Security.SuperUserKey, + new UserCreateModel() + { + Email = username, + Name = username, + UserName = username, + UserGroupKeys = new HashSet(new[] { Constants.Security.EditorGroupKey }) + }, + true)).Result.CreatedUser; + userKey = user.Key; + } + + + var token = await userManager.GeneratePasswordResetTokenAsync(user); + + + var changePasswordAttempt = await userService.ChangePasswordAsync(userKey, + new ChangeUserPasswordModel + { + NewPassword = password, + ResetPasswordToken = token.Result.ToUrlBase64(), + UserKey = userKey + }); + + Assert.IsTrue(changePasswordAttempt.Success); + + var backOfficeApplicationManager = + serviceScope.ServiceProvider.GetRequiredService() as + BackOfficeApplicationManager; + backofficeOpenIddictApplicationDescriptor = + backOfficeApplicationManager.BackofficeOpenIddictApplicationDescriptor(client.BaseAddress); + scope.Complete(); + } + + var loginModel = new BackOfficeController.LoginRequestModel { Username = username, Password = password }; + + // Login to ensure the cookie is set (used in next request) + var loginResponse = await client.PostAsync( + GetManagementApiUrl(x => x.Login(null)), JsonContent.Create(loginModel)); + + Assert.AreEqual(HttpStatusCode.OK, loginResponse.StatusCode, await loginResponse.Content.ReadAsStringAsync()); + + var codeVerifier = "12345"; // Just a dummy value we use in tests + var codeChallange = Convert.ToBase64String(SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(codeVerifier))) + .TrimEnd("="); + + var authorizeResponse = await client.GetAsync( + GetManagementApiUrl(x => x.Authorize()) + + $"?client_id={backofficeOpenIddictApplicationDescriptor.ClientId}&response_type=code&redirect_uri={WebUtility.UrlEncode(backofficeOpenIddictApplicationDescriptor.RedirectUris.FirstOrDefault()?.AbsoluteUri)}&code_challenge_method=S256&code_challenge={codeChallange}"); + + Assert.AreEqual(HttpStatusCode.Found, authorizeResponse.StatusCode, await authorizeResponse.Content.ReadAsStringAsync()); + + var tokenResponse = await client.PostAsync("/umbraco/management/api/v1/security/back-office/token", + new FormUrlEncodedContent(new Dictionary + { + ["grant_type"] = "authorization_code", + ["code_verifier"] = codeVerifier, + ["client_id"] = backofficeOpenIddictApplicationDescriptor.ClientId, + ["code"] = HttpUtility.ParseQueryString(authorizeResponse.Headers.Location.Query).Get("code"), + ["redirect_uri"] = + backofficeOpenIddictApplicationDescriptor.RedirectUris.FirstOrDefault().AbsoluteUri + })); + + var tokenModel = await tokenResponse.Content.ReadFromJsonAsync(); + + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenModel.AccessToken); + } + + private class TokenModel + { + [JsonPropertyName("access_token")] public string AccessToken { get; set; } + } + +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Policies/ByKeyAuditLogControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Policies/ByKeyAuditLogControllerTests.cs new file mode 100644 index 0000000000..c5145ca678 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Policies/ByKeyAuditLogControllerTests.cs @@ -0,0 +1,46 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Headers; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.AuditLog; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Policies; + +/// +/// +/// +[TestFixture] +public class ByKeyAuditLogControllerTests : ManagementApiTest +{ + protected override Expression> MethodSelector => + x => x.ByKey(Constants.Security.SuperUserKey, Direction.Ascending, null, 0, 100); + + [Test] + public virtual async Task As_Admin_I_Have_Access() + { + await AuthenticateClientAsync(Client, "admin@umbraco.com", "1234567890", true); + + var response = await Client.GetAsync(Url); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, await response.Content.ReadAsStringAsync()); + } + + [Test] + public virtual async Task As_Editor_I_Have_Access() + { + await AuthenticateClientAsync(Client, "editor@umbraco.com", "1234567890", false); + + var response = await Client.GetAsync(Url); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, await response.Content.ReadAsStringAsync()); + } + + [Test] + public virtual async Task Unauthourized_when_no_token_is_provided() + { + var response = await Client.GetAsync(Url); + + Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode, await response.Content.ReadAsStringAsync()); + } +} diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 0ab4116713..bb6314aab1 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using System.Reflection; +using Asp.Versioning; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -12,6 +13,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; using NUnit.Framework; +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Api.Delivery.Controllers.Content; +using Umbraco.Cms.Api.Management.Controllers; +using Umbraco.Cms.Api.Management.Controllers.ModelsBuilder; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Services; @@ -34,7 +40,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest { protected HttpClient Client { get; private set; } - protected LinkGenerator LinkGenerator { get; private set; } + protected LinkGenerator LinkGenerator => Factory.Services.GetRequiredService(); protected WebApplicationFactory Factory { get; private set; } @@ -48,6 +54,14 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest { } + protected virtual void CustomTestAuthSetup(IServiceCollection services) + { + // Add a test auth scheme with a test auth handler to authn and assign the user + services.AddAuthentication(TestAuthHandler.TestAuthenticationScheme) + .AddScheme( + TestAuthHandler.TestAuthenticationScheme, options => { }); + } + [SetUp] public void Setup() { @@ -68,6 +82,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest */ var factory = new UmbracoWebApplicationFactory(CreateHostBuilder); + // additional host configuration for web server integration tests Factory = factory.WithWebHostBuilder(builder => { @@ -79,19 +94,15 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest { services.AddSingleton(); - // Add a test auth scheme with a test auth handler to authn and assign the user - services.AddAuthentication(TestAuthHandler.TestAuthenticationScheme) - .AddScheme( - TestAuthHandler.TestAuthenticationScheme, options => { }); + CustomTestAuthSetup(services); }); }); Client = Factory.CreateClient(new WebApplicationFactoryClientOptions { - AllowAutoRedirect = false + AllowAutoRedirect = false, + BaseAddress = new Uri("https://localhost/", UriKind.Absolute) }); - - LinkGenerator = Factory.Services.GetRequiredService(); } /// @@ -106,6 +117,25 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest return PrepareUrl(url); } + protected string GetManagementApiUrl(Expression> methodSelector) + where T : ManagementApiControllerBase + { + MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector); + IDictionary methodParams = ExpressionHelper.GetMethodParams(methodSelector) ?? new Dictionary(); + + + methodParams["version"] = method?.GetCustomAttribute()?.Versions?.First().MajorVersion.ToString(); + if (method == null) + { + throw new MissingMethodException( + $"Could not find the method {methodSelector} on type {typeof(T)} or the result "); + } + + var controllerName = ControllerExtensions.GetControllerName(typeof(T)); + + return LinkGenerator.GetUmbracoControllerUrl(method.Name, controllerName, null, methodParams); + } + /// /// Prepare a url before using . /// This returns the url but also sets the HttpContext.request into to use this url. @@ -212,6 +242,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest services.AddLogger(TestHelper.GetWebHostEnvironment(), Configuration); var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory, TestHelper.Profiler, AppCaches.NoCache, hostingEnvironment); + builder.Services.AddTransient(sp => new TestDatabaseHostedLifecycleService(() => UseTestDatabase(sp))); builder .AddConfiguration() @@ -236,6 +267,12 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest // Adds Umbraco.Web.Website mvcBuilder.AddApplicationPart(typeof(SurfaceController).Assembly); + // Adds Umbraco.Cms.Api.ManagementApi + mvcBuilder.AddApplicationPart(typeof(ModelsBuilderControllerBase).Assembly); + + // Adds Umbraco.Cms.Api.DeliveryApi + mvcBuilder.AddApplicationPart(typeof(ContentApiItemControllerBase).Assembly); + // Adds Umbraco.Tests.Integration mvcBuilder.AddApplicationPart(typeof(UmbracoTestServerTestBase).Assembly); }) @@ -244,11 +281,13 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest .AddUmbracoSqlServerSupport() .AddUmbracoSqliteSupport() .AddDeliveryApi() + .AddUmbracoManagementApi() .AddComposers() .AddTestServices(TestHelper); // This is the important one! CustomTestSetup(builder); + builder.Build(); } @@ -262,8 +301,6 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest protected void Configure(IApplicationBuilder app) { - UseTestDatabase(app); - app.UseUmbraco() .WithMiddleware(u => { @@ -272,9 +309,36 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest }) .WithEndpoints(u => { + u.UseInstallerEndpoints(); u.UseBackOfficeEndpoints(); u.UseWebsiteEndpoints(); }); } } } + +public class TestDatabaseHostedLifecycleService : IHostedLifecycleService +{ + private readonly Action _action; + + public TestDatabaseHostedLifecycleService(Action action) + { + _action = action; + } + + public Task StartAsync(CancellationToken cancellationToken)=> Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StartingAsync(CancellationToken cancellationToken) + { + _action(); + return Task.CompletedTask; + + } + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs index 902326973b..414fe0840c 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs @@ -2,7 +2,9 @@ // See LICENSE for more details. using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Tests.Integration.TestServerTest;