diff --git a/.github/release.yml b/.github/release.yml index 9b9f32cdde..e65616aafe 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -17,6 +17,7 @@ changelog: labels: - type/bug - category/bug + - type/improvement - title: 📄 Documentation labels: - category/documentation @@ -30,5 +31,8 @@ changelog: labels: - category/accessibility - title: 🚀 New Features + labels: + - type/feature + - title: Other Changes labels: - '*' diff --git a/Directory.Packages.props b/Directory.Packages.props index c652e5b7ed..9794b5a08a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,7 +5,7 @@ - + @@ -44,22 +44,22 @@ - - + + - + - - - + + + @@ -68,7 +68,7 @@ - + @@ -79,7 +79,7 @@ - + @@ -87,4 +87,4 @@ - \ No newline at end of file + diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 7d89c8b97f..977ce443d0 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -63,15 +63,14 @@ variables: DOTNET_GENERATE_ASPNET_CERTIFICATE: false DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true + npm_config_cache: $(Pipeline.Workspace)/.npm_client + NODE_OPTIONS: --max_old_space_size=16384 stages: ############################################### ## Build ############################################### - stage: Build - variables: - npm_config_cache: $(Pipeline.Workspace)/.npm_client - NODE_OPTIONS: --max_old_space_size=16384 jobs: - job: A displayName: Build Umbraco CMS @@ -80,18 +79,11 @@ stages: steps: - checkout: self submodules: true - - task: NodeTool@0 - displayName: Use Node.js $(nodeVersion) - retryCountOnTaskFailure: 3 + - task: UseDotNet@2 + displayName: Use .NET SDK from global.json inputs: - versionSpec: $(nodeVersion) - - script: npm ci --no-fund --no-audit --prefer-offline - displayName: Run npm ci (Bellissima) - workingDirectory: src/Umbraco.Web.UI.Client - - script: npm run generate:api-local - displayName: Generate API models (Bellissima) - workingDirectory: src/Umbraco.Web.UI.Client - enabled: false + useGlobalJson: true + - template: templates/backoffice-install.yml - script: npm run build:for:cms displayName: Run build (Bellissima) workingDirectory: src/Umbraco.Web.UI.Client @@ -101,10 +93,6 @@ stages: - script: npm run build displayName: Run npm build (Login) workingDirectory: src/Umbraco.Web.UI.Login - - task: UseDotNet@2 - displayName: Use .NET SDK from global.json - inputs: - useGlobalJson: true - task: DotNetCoreCLI@2 displayName: Run dotnet restore inputs: @@ -127,18 +115,25 @@ stages: inputs: targetPath: $(Build.SourcesDirectory) artifactName: build_output + + - job: B + displayName: Build Bellissima Package + pool: + vmImage: 'ubuntu-latest' + steps: + - checkout: self + submodules: true + - template: templates/backoffice-install.yml + - script: npm run build:for:npm + displayName: Run build:for:npm + workingDirectory: src/Umbraco.Web.UI.Client - bash: | - echo "##[command]Running npm version" - echo "##[debug]Version: $PACKAGE_VERSION" echo "##[command]Running npm pack" echo "##[debug]Output directory: $(Build.ArtifactStagingDirectory)" - npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version mkdir $(Build.ArtifactStagingDirectory)/npm npm pack --pack-destination $(Build.ArtifactStagingDirectory)/npm mv .npmrc $(Build.ArtifactStagingDirectory)/npm/ - displayName: Prepare Bellissima npm package - env: - PACKAGE_VERSION: $(build.NBGV_NpmPackageVersion) + displayName: Run npm pack workingDirectory: src/Umbraco.Web.UI.Client - task: PublishPipelineArtifact@1 displayName: Publish Bellissima npm artifact @@ -208,28 +203,11 @@ stages: pool: vmImage: 'ubuntu-latest' variables: - npm_config_cache: $(Pipeline.Workspace)/.npm_client - NODE_OPTIONS: --max_old_space_size=16384 BASE_PATH: /v$(umbracoMajorVersion)/ui steps: - checkout: self submodules: true - - task: NodeTool@0 - displayName: Use Node.js $(nodeVersion) - retryCountOnTaskFailure: 3 - inputs: - versionSpec: $(nodeVersion) - - task: Cache@2 - displayName: Cache node_modules - inputs: - key: '"npm_client" | "$(Agent.OS)"| $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json' - restoreKeys: | - "npm_client" | "$(Agent.OS)" - "npm_client" - path: $(npm_config_cache) - - script: npm ci --no-fund --no-audit --prefer-offline - workingDirectory: src/Umbraco.Web.UI.Client - displayName: Run npm ci + - template: templates/backoffice-install.yml - script: npm run storybook:build displayName: Build Storybook env: @@ -239,16 +217,30 @@ stages: displayName: Replace BASE_PATH on assets workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/storybook-static - task: ArchiveFiles@2 - displayName: Archive js Docs + displayName: Archive Storybook inputs: rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/storybook-static includeRootFolder: false archiveFile: $(Build.ArtifactStagingDirectory)/ui-docs.zip - task: PublishPipelineArtifact@1 - displayName: Publish js Docs + displayName: Publish Storybook inputs: targetPath: $(Build.ArtifactStagingDirectory)/ui-docs.zip artifact: ui-docs + - script: npm run generate:ui-api-docs + displayName: Generate API Docs + workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client + - task: ArchiveFiles@2 + displayName: Archive UI API Docs + inputs: + rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/ui-api + includeRootFolder: false + archiveFile: $(Build.ArtifactStagingDirectory)/ui-api-docs.zip + - task: PublishPipelineArtifact@1 + displayName: Publish UI API Docs + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/ui-api-docs.zip + artifact: ui-api-docs ############################################### ## Test @@ -868,7 +860,7 @@ stages: BlobPrefix: v$(umbracoMajorVersion)/csharp CleanTargetBeforeCopy: true - job: - displayName: Upload js Docs + displayName: Upload Storybook steps: - checkout: none - task: DownloadPipelineArtifact@2 @@ -881,7 +873,7 @@ stages: archiveFilePatterns: $(Build.SourcesDirectory)/ui-docs.zip destinationFolder: $(Build.ArtifactStagingDirectory)/ui-docs - task: AzureFileCopy@4 - displayName: 'Copy UI Docs to blob storage' + displayName: 'Copy Storybook to blob storage' inputs: SourcePath: '$(Build.ArtifactStagingDirectory)/ui-docs/*' azureSubscription: umbraco-storage @@ -890,3 +882,26 @@ stages: ContainerName: '$web' BlobPrefix: v$(umbracoMajorVersion)/ui CleanTargetBeforeCopy: true + - job: + displayName: Upload UI API Docs + steps: + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download artifact + inputs: + artifact: ui-api-docs + path: $(Build.SourcesDirectory) + - task: ExtractFiles@1 + inputs: + archiveFilePatterns: $(Build.SourcesDirectory)/ui-api-docs.zip + destinationFolder: $(Build.ArtifactStagingDirectory)/ui-api-docs + - task: AzureFileCopy@4 + displayName: 'Copy UI API Docs to blob storage' + inputs: + SourcePath: '$(Build.ArtifactStagingDirectory)/ui-api-docs/*' + azureSubscription: umbraco-storage + Destination: AzureBlob + storage: umbracoapidocs + ContainerName: '$web' + BlobPrefix: v$(umbracoMajorVersion)/ui-api + CleanTargetBeforeCopy: true diff --git a/build/templates/backoffice-install.yml b/build/templates/backoffice-install.yml new file mode 100644 index 0000000000..2c22cca7e3 --- /dev/null +++ b/build/templates/backoffice-install.yml @@ -0,0 +1,31 @@ +steps: + - task: NodeTool@0 + displayName: Use Node.js + retryCountOnTaskFailure: 3 + inputs: + versionSource: 'fromFile' + versionFilePath: src/Umbraco.Web.UI.Client/.nvmrc + + - bash: | + echo "##[command]Install nbgv" + dotnet tool install --tool-path . nbgv + echo "##[command]Running nbgv get-version" + PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion) + echo "##[command]Running npm version" + echo "##[debug]Version: $PACKAGE_VERSION" + cd src/Umbraco.Web.UI.Client + npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version + displayName: Set NPM Version + + - task: Cache@2 + displayName: Cache node_modules + inputs: + key: '"npm_client" | "$(Agent.OS)"| $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json' + restoreKeys: | + "npm_client" | "$(Agent.OS)" + "npm_client" + path: $(npm_config_cache) + + - script: npm ci --no-fund --no-audit --prefer-offline + displayName: Run npm ci (Bellissima) + workingDirectory: src/Umbraco.Web.UI.Client diff --git a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureOpenIddict.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureOpenIddict.cs new file mode 100644 index 0000000000..f428957bd9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureOpenIddict.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Options; +using OpenIddict.Server.AspNetCore; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Api.Common.Configuration; + +internal class ConfigureOpenIddict : IConfigureOptions +{ + private readonly IOptions _globalSettings; + + public ConfigureOpenIddict(IOptions globalSettings) => _globalSettings = globalSettings; + + public void Configure(OpenIddictServerAspNetCoreOptions options) + => options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false; +} diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs index b74727ff52..49fe1f233e 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs @@ -11,7 +11,7 @@ public static class UmbracoBuilderApiExtensions { public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builder) { - if (builder.Services.Any(x => x.ImplementationType == typeof(OperationIdSelector))) + if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OperationIdSelector))) { return builder; } diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs index 7e730695f3..98068791af 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using OpenIddict.Server; using OpenIddict.Validation; +using Umbraco.Cms.Api.Common.Configuration; using Umbraco.Cms.Api.Common.Security; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -17,7 +18,7 @@ public static class UmbracoBuilderAuthExtensions { public static IUmbracoBuilder AddUmbracoOpenIddict(this IUmbracoBuilder builder) { - if (builder.Services.Any(x=>x.ImplementationType == typeof(OpenIddictCleanupJob)) is false) + if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OpenIddictCleanupJob)) is false) { ConfigureOpenIddict(builder); } @@ -132,5 +133,6 @@ public static class UmbracoBuilderAuthExtensions }); builder.Services.AddRecurringBackgroundJob(); + builder.Services.ConfigureOptions(); } } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs index 08d7a916e6..37651a4158 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs @@ -8,11 +8,5 @@ internal abstract class RequestHeaderHandler protected RequestHeaderHandler(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; - protected string? GetHeaderValue(string headerName) - { - HttpContext httpContext = _httpContextAccessor.HttpContext ?? - throw new InvalidOperationException("Could not obtain an HTTP context"); - - return httpContext.Request.Headers[headerName]; - } + protected string? GetHeaderValue(string headerName) => _httpContextAccessor.HttpContext?.Request.Headers[headerName]; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ConfigurationDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ConfigurationDocumentTypeController.cs index e7a785cf88..c1a4e41a75 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ConfigurationDocumentTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ConfigurationDocumentTypeController.cs @@ -2,9 +2,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Features; using Umbraco.Cms.Web.Common.Authorization; @@ -14,18 +17,31 @@ namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] public class ConfigurationDocumentTypeController : DocumentTypeControllerBase { - private readonly UmbracoFeatures _umbracoFeatures; - private readonly DataTypesSettings _dataTypesSettings; - private readonly SegmentSettings _segmentSettings; + private readonly IConfigurationPresentationFactory _configurationPresentationFactory; + [ActivatorUtilitiesConstructor] + public ConfigurationDocumentTypeController(IConfigurationPresentationFactory configurationPresentationFactory) + { + _configurationPresentationFactory = configurationPresentationFactory; + } + + [Obsolete("Use the constructor that only accepts IConfigurationPresentationFactory, scheduled for removal in V16")] + public ConfigurationDocumentTypeController( + UmbracoFeatures umbracoFeatures, + IOptionsSnapshot dataTypesSettings, + IOptionsSnapshot segmentSettings, + IConfigurationPresentationFactory configurationPresentationFactory) + : this(configurationPresentationFactory) + { + } + + [Obsolete("Use the constructor that only accepts IConfigurationPresentationFactory, scheduled for removal in V16")] public ConfigurationDocumentTypeController( UmbracoFeatures umbracoFeatures, IOptionsSnapshot dataTypesSettings, IOptionsSnapshot segmentSettings) + : this(StaticServiceProvider.Instance.GetRequiredService()) { - _umbracoFeatures = umbracoFeatures; - _dataTypesSettings = dataTypesSettings.Value; - _segmentSettings = segmentSettings.Value; } [HttpGet("configuration")] @@ -33,12 +49,7 @@ public class ConfigurationDocumentTypeController : DocumentTypeControllerBase [ProducesResponseType(typeof(DocumentTypeConfigurationResponseModel), StatusCodes.Status200OK)] public Task Configuration(CancellationToken cancellationToken) { - var responseModel = new DocumentTypeConfigurationResponseModel - { - DataTypesCanBeChanged = _dataTypesSettings.CanBeChanged, - DisableTemplates = _umbracoFeatures.Disabled.DisableTemplates, - UseSegments = _segmentSettings.Enabled, - }; + DocumentTypeConfigurationResponseModel responseModel = _configurationPresentationFactory.CreateDocumentTypeConfigurationResponseModel(); return Task.FromResult(Ok(responseModel)); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs index 2e5b7385ae..c65ed1631d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs @@ -86,6 +86,18 @@ public abstract class DocumentTypeControllerBase : ManagementApiControllerBase .WithTitle("Operation not permitted") .WithDetail("The attempted operation was not permitted, likely due to a permission/configuration mismatch with the operation.") .Build()), + ContentTypeOperationStatus.CancelledByNotification => new BadRequestObjectResult(problemDetailsBuilder + .WithTitle("Cancelled by notification") + .WithDetail("The attempted operation was cancelled by a notification.") + .Build()), + ContentTypeOperationStatus.NameCannotBeEmpty => new BadRequestObjectResult(problemDetailsBuilder + .WithTitle("Name cannot be empty") + .WithDetail("The name of the content type cannot be empty") + .Build()), + ContentTypeOperationStatus.NameTooLong => new BadRequestObjectResult(problemDetailsBuilder + .WithTitle("Name was too long") + .WithDetail("Name cannot be more than 255 characters in length.") + .Build()), _ => new ObjectResult("Unknown content type operation status") { StatusCode = StatusCodes.Status500InternalServerError }, }); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ConfigurationMediaTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ConfigurationMediaTypeController.cs new file mode 100644 index 0000000000..25447c60a1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ConfigurationMediaTypeController.cs @@ -0,0 +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.ViewModels.MediaType; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Api.Management.Controllers.MediaType; + +[ApiVersion("1.0")] +[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)] +public class ConfigurationMediaTypeController : MediaTypeControllerBase +{ + private readonly IConfigurationPresentationFactory _configurationPresentationFactory; + + public ConfigurationMediaTypeController(IConfigurationPresentationFactory configurationPresentationFactory) + { + _configurationPresentationFactory = configurationPresentationFactory; + } + + [HttpGet("configuration")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(MediaTypeConfigurationResponseModel), StatusCodes.Status200OK)] + public Task Configuration(CancellationToken cancellationToken) + { + MediaTypeConfigurationResponseModel responseModel = _configurationPresentationFactory.CreateMediaTypeConfigurationResponseModel(); + + return Task.FromResult(Ok(responseModel)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/ConfigurationMemberTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/ConfigurationMemberTypeController.cs new file mode 100644 index 0000000000..141dbd80ee --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/ConfigurationMemberTypeController.cs @@ -0,0 +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.ViewModels.MemberType; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Api.Management.Controllers.MemberType; + +[ApiVersion("1.0")] +public class ConfigurationMemberTypeController : MemberTypeControllerBase +{ + private readonly IConfigurationPresentationFactory _configurationPresentationFactory; + + public ConfigurationMemberTypeController(IConfigurationPresentationFactory configurationPresentationFactory) + { + _configurationPresentationFactory = configurationPresentationFactory; + } + + [HttpGet("configuration")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(MemberTypeConfigurationResponseModel), StatusCodes.Status200OK)] + public Task Configuration(CancellationToken cancellationToken) + { + MemberTypeConfigurationResponseModel responseModel = _configurationPresentationFactory.CreateMemberTypeConfigurationResponseModel(); + + return Task.FromResult(Ok(responseModel)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Relation/ByRelationTypeKeyRelationController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Relation/ByRelationTypeKeyRelationController.cs index f0aee0317f..cde52b5884 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Relation/ByRelationTypeKeyRelationController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Relation/ByRelationTypeKeyRelationController.cs @@ -1,4 +1,4 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.ViewModels.Pagination; @@ -29,7 +29,7 @@ public class ByRelationTypeKeyRelationController : RelationControllerBase /// /// Use case: On a relation type page you can see all created relations of this type. /// - [HttpGet("type/{id:guid}")] + [HttpGet("type/{id:guid}", Name = "GetRelationByRelationTypeId")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status404NotFound)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/CalculateStartNodesUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/CalculateStartNodesUserController.cs new file mode 100644 index 0000000000..98f1526ce9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/CalculateStartNodesUserController.cs @@ -0,0 +1,59 @@ +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.ViewModels.User; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security.Authorization; +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 CalculatedStartNodesUserController : UserControllerBase +{ + private readonly IAuthorizationService _authorizationService; + private readonly IUserService _userService; + private readonly IUserPresentationFactory _userPresentationFactory; + + public CalculatedStartNodesUserController( + IAuthorizationService authorizationService, + IUserService userService, + IUserPresentationFactory userPresentationFactory) + { + _authorizationService = authorizationService; + _userService = userService; + _userPresentationFactory = userPresentationFactory; + } + + [HttpGet("{id:guid}/calculate-start-nodes")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(CalculatedUserStartNodesResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task CalculatedStartNodes(CancellationToken cancellationToken, Guid id) + { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(id), + AuthorizationPolicies.UserPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + + IUser? user = await _userService.GetAsync(id); + + if (user is null) + { + return UserOperationStatusResult(UserOperationStatus.UserNotFound); + } + + CalculatedUserStartNodesResponseModel responseModel = await _userPresentationFactory.CreateCalculatedUserStartNodesResponseModelAsync(user); + return Ok(responseModel); + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index e36f103e92..5ddb98b570 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -27,11 +27,8 @@ public static class BackOfficeAuthBuilderExtensions public static IUmbracoBuilder AddTokenRevocation(this IUmbracoBuilder builder) { - builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); return builder; diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs index 4714c54c74..45eccad5ec 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs @@ -28,6 +28,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddAuthorization(CreatePolicies); return builder; @@ -35,14 +36,12 @@ internal static class BackOfficeAuthPolicyBuilderExtensions private static void CreatePolicies(AuthorizationOptions options) { - void AddPolicy(string policyName, string claimType, params string[] allowedClaimValues) - { - options.AddPolicy(policyName, policy => + void AddAllowedApplicationsPolicy(string policyName, params string[] allowedClaimValues) + => options.AddPolicy(policyName, policy => { policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); - policy.RequireClaim(claimType, allowedClaimValues); + policy.Requirements.Add(new AllowedApplicationRequirement(allowedClaimValues)); }); - } options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { @@ -56,39 +55,39 @@ internal static class BackOfficeAuthPolicyBuilderExtensions 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, + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessContent, Constants.Applications.Content); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Applications.Content, Constants.Applications.Media); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForContentTree, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessForMediaTree, Constants.Security.AllowedApplicationsClaimType, + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForMediaTree, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessForMemberTree, Constants.Security.AllowedApplicationsClaimType, + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForMemberTree, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Media); - AddPolicy(AuthorizationPolicies.SectionAccessMembers, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessPackages, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Packages); - AddPolicy(AuthorizationPolicies.SectionAccessSettings, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.SectionAccessUsers, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Users); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Applications.Media); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMembers, Constants.Applications.Members); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessPackages, Constants.Applications.Packages); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessSettings, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessUsers, Constants.Applications.Users); - AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessDictionary, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Translation); - AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Translation, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content); - AddPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessLanguages, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Media, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessScripts, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessStylesheets, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessTemplates, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessWebhooks, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDataTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDictionary, Constants.Applications.Translation); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, Constants.Applications.Translation, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Applications.Content); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Applications.Content, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessLanguages, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, Constants.Applications.Media, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMemberGroups, Constants.Applications.Members); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMemberTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessPartialViews, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessRelationTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessScripts, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessStylesheets, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessTemplates, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessWebhooks, Constants.Applications.Settings); // Contextual permissions options.AddPolicy(AuthorizationPolicies.ContentPermissionByResource, policy => diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeCorsPolicyBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeCorsPolicyBuilderExtensions.cs index da81e5de7f..0f8a57f5d8 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeCorsPolicyBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeCorsPolicyBuilderExtensions.cs @@ -34,7 +34,7 @@ internal static class BackOfficeCorsPolicyBuilderExtensions { policy .WithOrigins(customOrigin) - .WithExposedHeaders(Constants.Headers.Location, Constants.Headers.GeneratedResource) + .WithExposedHeaders(Constants.Headers.Location, Constants.Headers.GeneratedResource, Constants.Headers.Notifications) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs index 9acd86381c..90993aa382 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs @@ -24,7 +24,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.AddUmbracoApiOpenApiUI(); - if (!services.Any(x => x.ImplementationType == typeof(JsonPatchService))) + if (!services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(JsonPatchService))) { ModelsBuilderBuilderExtensions.AddModelsBuilder(builder) .AddJson() diff --git a/src/Umbraco.Cms.Api.Management/Factories/ConfigurationPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/ConfigurationPresentationFactory.cs index b8580083c5..7efc4ff90f 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/ConfigurationPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/ConfigurationPresentationFactory.cs @@ -1,8 +1,14 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; using Umbraco.Cms.Api.Management.ViewModels.Media; +using Umbraco.Cms.Api.Management.ViewModels.MediaType; using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Features; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Factories; @@ -10,19 +16,40 @@ namespace Umbraco.Cms.Api.Management.Factories; public class ConfigurationPresentationFactory : IConfigurationPresentationFactory { private readonly IReservedFieldNamesService _reservedFieldNamesService; + private readonly UmbracoFeatures _umbracoFeatures; + private readonly DataTypesSettings _dataTypesSettings; private readonly ContentSettings _contentSettings; private readonly SegmentSettings _segmentSettings; public ConfigurationPresentationFactory( IReservedFieldNamesService reservedFieldNamesService, IOptions contentSettings, - IOptions segmentSettings) + IOptions segmentSettings, + IOptions dataTypesSettings, + UmbracoFeatures umbracoFeatures) { _reservedFieldNamesService = reservedFieldNamesService; + _umbracoFeatures = umbracoFeatures; + _dataTypesSettings = dataTypesSettings.Value; _contentSettings = contentSettings.Value; _segmentSettings = segmentSettings.Value; } + [Obsolete("Use the constructor with all dependencies")] + public ConfigurationPresentationFactory( + IReservedFieldNamesService reservedFieldNamesService, + IOptions contentSettings, + IOptions segmentSettings) + : this( + reservedFieldNamesService, + contentSettings, + segmentSettings, + StaticServiceProvider.Instance.GetRequiredService>(), + StaticServiceProvider.Instance.GetRequiredService() + ) + { + } + public DocumentConfigurationResponseModel CreateDocumentConfigurationResponseModel() => new() { @@ -33,12 +60,27 @@ public class ConfigurationPresentationFactory : IConfigurationPresentationFactor ReservedFieldNames = _reservedFieldNamesService.GetDocumentReservedFieldNames(), }; + public DocumentTypeConfigurationResponseModel CreateDocumentTypeConfigurationResponseModel() => + new() + { + DataTypesCanBeChanged = _dataTypesSettings.CanBeChanged, + DisableTemplates = _umbracoFeatures.Disabled.DisableTemplates, + UseSegments = _segmentSettings.Enabled, + ReservedFieldNames = _reservedFieldNamesService.GetDocumentReservedFieldNames(), + }; + public MemberConfigurationResponseModel CreateMemberConfigurationResponseModel() => new() { ReservedFieldNames = _reservedFieldNamesService.GetMemberReservedFieldNames(), }; + public MemberTypeConfigurationResponseModel CreateMemberTypeConfigurationResponseModel() => + new() + { + ReservedFieldNames = _reservedFieldNamesService.GetMemberReservedFieldNames(), + }; + public MediaConfigurationResponseModel CreateMediaConfigurationResponseModel() => new() { @@ -46,4 +88,10 @@ public class ConfigurationPresentationFactory : IConfigurationPresentationFactor DisableUnpublishWhenReferenced = _contentSettings.DisableUnpublishWhenReferenced, ReservedFieldNames = _reservedFieldNamesService.GetMediaReservedFieldNames(), }; + + public MediaTypeConfigurationResponseModel CreateMediaTypeConfigurationResponseModel() => + new() + { + ReservedFieldNames = _reservedFieldNamesService.GetMediaReservedFieldNames(), + }; } diff --git a/src/Umbraco.Cms.Api.Management/Factories/IConfigurationPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IConfigurationPresentationFactory.cs index 920a7fd7b8..3914a0e058 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IConfigurationPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IConfigurationPresentationFactory.cs @@ -1,6 +1,9 @@ using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; using Umbraco.Cms.Api.Management.ViewModels.Media; +using Umbraco.Cms.Api.Management.ViewModels.MediaType; using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Api.Management.ViewModels.MemberType; namespace Umbraco.Cms.Api.Management.Factories; @@ -8,7 +11,16 @@ public interface IConfigurationPresentationFactory { DocumentConfigurationResponseModel CreateDocumentConfigurationResponseModel(); + DocumentTypeConfigurationResponseModel CreateDocumentTypeConfigurationResponseModel() + => throw new NotImplementedException(); + MemberConfigurationResponseModel CreateMemberConfigurationResponseModel(); + MemberTypeConfigurationResponseModel CreateMemberTypeConfigurationResponseModel() + => throw new NotImplementedException(); + MediaConfigurationResponseModel CreateMediaConfigurationResponseModel(); + + MediaTypeConfigurationResponseModel CreateMediaTypeConfigurationResponseModel() + => throw new NotImplementedException(); } diff --git a/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs index 9af4ace9e1..a1e8a79edf 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs @@ -25,4 +25,6 @@ public interface IUserPresentationFactory Task CreateCurrentUserConfigurationModelAsync(); UserItemResponseModel CreateItemResponseModel(IUser user); + + Task CreateCalculatedUserStartNodesResponseModelAsync(IUser user); } diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs index 641f5883ed..59e94b5d43 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs @@ -150,6 +150,7 @@ public class UserPresentationFactory : IUserPresentationFactory { // You should not be able to invite users if any providers has deny local login set. CanInviteUsers = _emailSender.CanSendRequiredEmail() && _externalLoginProviders.HasDenyLocalLogin() is false, + UsernameIsEmail = _securitySettings.UsernameIsEmail, PasswordConfiguration = _passwordConfigurationPresentationFactory.CreatePasswordConfigurationResponseModel(), }); @@ -212,6 +213,23 @@ public class UserPresentationFactory : IUserPresentationFactory }); } + public async Task CreateCalculatedUserStartNodesResponseModelAsync(IUser user) + { + var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches); + ISet mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media); + var contentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches); + ISet documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document); + + return await Task.FromResult(new CalculatedUserStartNodesResponseModel() + { + Id = user.Key, + MediaStartNodeIds = mediaStartNodeKeys, + HasMediaRootAccess = HasRootAccess(mediaStartNodeIds), + DocumentStartNodeIds = documentStartNodeKeys, + HasDocumentRootAccess = HasRootAccess(contentStartNodeIds), + }); + } + private ISet GetKeysFromIds(IEnumerable? ids, UmbracoObjectTypes type) { IEnumerable? models = ids? diff --git a/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs b/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs index acf6cb508a..e56e776f85 100644 --- a/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs +++ b/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs @@ -13,77 +13,31 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Handlers; internal sealed class RevokeUserAuthenticationTokensNotificationHandler : - INotificationAsyncHandler, INotificationAsyncHandler, INotificationAsyncHandler, - INotificationAsyncHandler, - INotificationAsyncHandler, INotificationAsyncHandler { - private const string NotificationStateKey = "Umbraco.Cms.Api.Management.Handlers.RevokeUserAuthenticationTokensNotificationHandler"; - private readonly IUserService _userService; - private readonly IUserGroupService _userGroupService; private readonly IOpenIddictTokenManager _tokenManager; private readonly ILogger _logger; private readonly SecuritySettings _securitySettings; public RevokeUserAuthenticationTokensNotificationHandler( IUserService userService, - IUserGroupService userGroupService, IOpenIddictTokenManager tokenManager, ILogger logger, IOptions securitySettingsOptions) { _userService = userService; - _userGroupService = userGroupService; _tokenManager = tokenManager; _logger = logger; _securitySettings = securitySettingsOptions.Value; } - // We need to know the pre-saving state of the saved users in order to compare if their access has changed - public async Task HandleAsync(UserSavingNotification notification, CancellationToken cancellationToken) - { - try - { - var usersAccess = new Dictionary(); - foreach (IUser user in notification.SavedEntities) - { - UserStartNodesAndGroupAccess? priorUserAccess = await GetRelevantUserAccessDataByUserKeyAsync(user.Key); - if (priorUserAccess == null) - { - continue; - } - - usersAccess.Add(user.Key, priorUserAccess); - } - - notification.State[NotificationStateKey] = usersAccess; - } - catch (DbException e) - { - _logger.LogWarning(e, "This is expected when we upgrade from < Umbraco 14. Otherwise it should not happen"); - } - } - public async Task HandleAsync(UserSavedNotification notification, CancellationToken cancellationToken) { try { - Dictionary? preSavingUsersState = null; - - if (notification.State.TryGetValue(NotificationStateKey, out var value)) - { - preSavingUsersState = value as Dictionary; - } - - // If we have a new user, there is no token - if (preSavingUsersState is null || preSavingUsersState.Count == 0) - { - return; - } - foreach (IUser user in notification.SavedEntities) { if (user.IsSuper()) @@ -95,23 +49,6 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : if (user.IsLockedOut || user.IsApproved is false) { await RevokeTokensAsync(user); - continue; - } - - // Don't revoke admin tokens to prevent log out when accidental changes - if (user.IsAdmin()) - { - continue; - } - - // Check if the user access has changed - we also need to revoke all tokens in this case - if (preSavingUsersState.TryGetValue(user.Key, out UserStartNodesAndGroupAccess? preSavingState)) - { - UserStartNodesAndGroupAccess postSavingState = MapToUserStartNodesAndGroupAccess(user); - if (preSavingState.CompareAccess(postSavingState) == false) - { - await RevokeTokensAsync(user); - } } } } @@ -131,49 +68,6 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : } } - // We need to know the pre-deleting state of the users part of the deleted group to revoke their tokens - public async Task HandleAsync(UserGroupDeletingNotification notification, CancellationToken cancellationToken) - { - var usersInGroups = new Dictionary>(); - foreach (IUserGroup userGroup in notification.DeletedEntities) - { - var users = await GetUsersByGroupKeyAsync(userGroup.Key); - if (users == null) - { - continue; - } - - usersInGroups.Add(userGroup.Key, users); - } - - notification.State[NotificationStateKey] = usersInGroups; - } - - public async Task HandleAsync(UserGroupDeletedNotification notification, CancellationToken cancellationToken) - { - Dictionary>? preDeletingUsersInGroups = null; - - if (notification.State.TryGetValue(NotificationStateKey, out var value)) - { - preDeletingUsersInGroups = value as Dictionary>; - } - - if (preDeletingUsersInGroups is null) - { - return; - } - - // since the user group was deleted, we can only use the information we collected before the deletion - // this means that we will not be able to detect users in any groups that were eventually deleted (due to implementor/3th party supplier interference) - // that were not in the initial to be deleted list - foreach (IUser user in preDeletingUsersInGroups - .Where(group => notification.DeletedEntities.Any(entity => group.Key == entity.Key)) - .SelectMany(group => group.Value)) - { - await RevokeTokensAsync(user); - } - } - public async Task HandleAsync(UserLoginSuccessNotification notification, CancellationToken cancellationToken) { if (_securitySettings.AllowConcurrentLogins is false) @@ -190,29 +84,6 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : } } - // Get data about the user before saving - private async Task GetRelevantUserAccessDataByUserKeyAsync(Guid userKey) - { - IUser? user = await _userService.GetAsync(userKey); - - return user is null - ? null - : MapToUserStartNodesAndGroupAccess(user); - } - - private UserStartNodesAndGroupAccess MapToUserStartNodesAndGroupAccess(IUser user) - => new(user.Groups.Select(g => g.Key), user.StartContentIds, user.StartMediaIds); - - // Get data about the users part of a group before deleting it - private async Task?> GetUsersByGroupKeyAsync(Guid userGroupKey) - { - IUserGroup? userGroup = await _userGroupService.GetAsync(userGroupKey); - - return userGroup is null - ? null - : _userService.GetAllInGroup(userGroup.Id); - } - private async Task RevokeTokensAsync(IUser user) { _logger.LogInformation("Revoking active tokens for user with ID {id}", user.Id); @@ -236,35 +107,4 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : return null; } - - private class UserStartNodesAndGroupAccess - { - public IEnumerable GroupKeys { get; } - - public int[]? StartContentIds { get; } - - public int[]? StartMediaIds { get; } - - public UserStartNodesAndGroupAccess(IEnumerable groupKeys, int[]? startContentIds, int[]? startMediaIds) - { - GroupKeys = groupKeys; - StartContentIds = startContentIds; - StartMediaIds = startMediaIds; - } - - public bool CompareAccess(UserStartNodesAndGroupAccess other) - { - var areContentStartNodesEqual = (StartContentIds == null && other.StartContentIds == null) || - (StartContentIds != null && other.StartContentIds != null && - StartContentIds.SequenceEqual(other.StartContentIds)); - - var areMediaStartNodesEqual = (StartMediaIds == null && other.StartMediaIds == null) || - (StartMediaIds != null && other.StartMediaIds != null && - StartMediaIds.SequenceEqual(other.StartMediaIds)); - - return areContentStartNodesEqual && - areMediaStartNodesEqual && - GroupKeys.SequenceEqual(other.GroupKeys); - } - } } diff --git a/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs b/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs index c9179215df..4da821337d 100644 --- a/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs +++ b/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs @@ -1,10 +1,15 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Security; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Middleware; @@ -16,15 +21,40 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly IServiceProvider _serviceProvider; private readonly IRuntimeState _runtimeState; + private readonly IOptions _globalSettings; + private readonly IOptions _webRoutingSettings; + private readonly IHostingEnvironment _hostingEnvironment; + [Obsolete("Use the non-obsolete constructor. This will be removed in Umbraco 16.")] public BackOfficeAuthorizationInitializationMiddleware( UmbracoRequestPaths umbracoRequestPaths, IServiceProvider serviceProvider, IRuntimeState runtimeState) + : this( + umbracoRequestPaths, + serviceProvider, + runtimeState, + StaticServiceProvider.Instance.GetRequiredService>(), + StaticServiceProvider.Instance.GetRequiredService>(), + StaticServiceProvider.Instance.GetRequiredService() + ) + { + } + + public BackOfficeAuthorizationInitializationMiddleware( + UmbracoRequestPaths umbracoRequestPaths, + IServiceProvider serviceProvider, + IRuntimeState runtimeState, + IOptions globalSettings, + IOptions webRoutingSettings, + IHostingEnvironment hostingEnvironment) { _umbracoRequestPaths = umbracoRequestPaths; _serviceProvider = serviceProvider; _runtimeState = runtimeState; + _globalSettings = globalSettings; + _webRoutingSettings = webRoutingSettings; + _hostingEnvironment = hostingEnvironment; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) @@ -47,6 +77,7 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware return; } + if (_umbracoRequestPaths.IsBackOfficeRequest(context.Request.Path) == false) { return; @@ -55,9 +86,13 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware await _firstBackOfficeRequestLocker.WaitAsync(); if (_firstBackOfficeRequest == false) { + Uri? backOfficeUrl = string.IsNullOrWhiteSpace(_webRoutingSettings.Value.UmbracoApplicationUrl) is false + ? new Uri($"{_webRoutingSettings.Value.UmbracoApplicationUrl.TrimEnd('/')}{_globalSettings.Value.GetBackOfficePath(_hostingEnvironment).EnsureStartsWith('/')}") + : null; + using IServiceScope scope = _serviceProvider.CreateScope(); IBackOfficeApplicationManager backOfficeApplicationManager = scope.ServiceProvider.GetRequiredService(); - await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(new Uri(context.Request.GetDisplayUrl())); + await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(backOfficeUrl ?? new Uri(context.Request.GetDisplayUrl())); _firstBackOfficeRequest = true; } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 0adc3c36b7..5d2d532ebc 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -14526,6 +14526,41 @@ ] } }, + "/umbraco/management/api/v1/media-type/configuration": { + "get": { + "tags": [ + "Media Type" + ], + "operationId": "GetMediaTypeConfiguration", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MediaTypeConfigurationResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/media-type/folder": { "post": { "tags": [ @@ -19094,6 +19129,41 @@ ] } }, + "/umbraco/management/api/v1/member-type/configuration": { + "get": { + "tags": [ + "Member Type" + ], + "operationId": "GetMemberTypeConfiguration", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberTypeConfigurationResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/tree/member-type/root": { "get": { "tags": [ @@ -23174,7 +23244,7 @@ "tags": [ "Relation" ], - "operationId": "GetRelationTypeById", + "operationId": "GetRelationByRelationTypeId", "parameters": [ { "name": "id", @@ -30298,6 +30368,66 @@ ] } }, + "/umbraco/management/api/v1/user/{id}/calculate-start-nodes": { + "get": { + "tags": [ + "User" + ], + "operationId": "GetUserByIdCalculateStartNodes", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CalculatedUserStartNodesResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/user/{id}/change-password": { "post": { "tags": [ @@ -33447,6 +33577,51 @@ }, "additionalProperties": false }, + "CalculatedUserStartNodesResponseModel": { + "required": [ + "documentStartNodeIds", + "hasDocumentRootAccess", + "hasMediaRootAccess", + "id", + "mediaStartNodeIds" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "documentStartNodeIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "hasDocumentRootAccess": { + "type": "boolean" + }, + "mediaStartNodeIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "hasMediaRootAccess": { + "type": "boolean" + } + }, + "additionalProperties": false + }, "ChangePasswordCurrentUserRequestModel": { "required": [ "newPassword" @@ -35176,7 +35351,8 @@ "type": "boolean" }, "usernameIsEmail": { - "type": "boolean" + "type": "boolean", + "deprecated": true }, "passwordConfiguration": { "oneOf": [ @@ -36019,7 +36195,8 @@ "type": "array", "items": { "type": "string" - } + }, + "deprecated": true } }, "additionalProperties": false @@ -36446,6 +36623,7 @@ "required": [ "dataTypesCanBeChanged", "disableTemplates", + "reservedFieldNames", "useSegments" ], "type": "object", @@ -36458,6 +36636,13 @@ }, "useSegments": { "type": "boolean" + }, + "reservedFieldNames": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false @@ -38133,7 +38318,8 @@ "type": "array", "items": { "type": "string" - } + }, + "deprecated": true } }, "additionalProperties": false @@ -38443,6 +38629,22 @@ }, "additionalProperties": false }, + "MediaTypeConfigurationResponseModel": { + "required": [ + "reservedFieldNames" + ], + "type": "object", + "properties": { + "reservedFieldNames": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "MediaTypeItemResponseModel": { "required": [ "id", @@ -38894,7 +39096,8 @@ "type": "array", "items": { "type": "string" - } + }, + "deprecated": true } }, "additionalProperties": false @@ -39125,6 +39328,22 @@ }, "additionalProperties": false }, + "MemberTypeConfigurationResponseModel": { + "required": [ + "reservedFieldNames" + ], + "type": "object", + "properties": { + "reservedFieldNames": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "MemberTypeItemResponseModel": { "required": [ "id", @@ -44331,13 +44550,17 @@ "UserConfigurationResponseModel": { "required": [ "canInviteUsers", - "passwordConfiguration" + "passwordConfiguration", + "usernameIsEmail" ], "type": "object", "properties": { "canInviteUsers": { "type": "boolean" }, + "usernameIsEmail": { + "type": "boolean" + }, "passwordConfiguration": { "oneOf": [ { diff --git a/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs b/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs index a854be29f0..a12e1acb2e 100644 --- a/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs +++ b/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs @@ -7,8 +7,6 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Web.Mvc; -using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Routing; using Umbraco.Extensions; @@ -54,8 +52,8 @@ public sealed class BackOfficeAreaRoutes : IAreaRoutes case RuntimeLevel.Install: case RuntimeLevel.Upgrade: case RuntimeLevel.Run: - MapMinimalBackOffice(endpoints); + endpoints.MapHub(_umbracoPathSegment + Constants.Web.BackofficeSignalRHub); break; case RuntimeLevel.BootFailed: case RuntimeLevel.Unknown: diff --git a/src/Umbraco.Cms.Api.Management/Routing/BackofficeHub.cs b/src/Umbraco.Cms.Api.Management/Routing/BackofficeHub.cs new file mode 100644 index 0000000000..47a2471a4b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Routing/BackofficeHub.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Umbraco.Cms.Api.Management.Routing; + +public class BackofficeHub : Hub +{ + public async Task SendPayload(object payload) => await Clients.All.SendAsync("payloadReceived", payload); +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs new file mode 100644 index 0000000000..a36a592827 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Authorization; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorizes that the current user has the correct permission access to the applications listed in the requirement. +/// +internal sealed class AllowedApplicationHandler : MustSatisfyRequirementAuthorizationHandler +{ + private readonly IAuthorizationHelper _authorizationHelper; + + public AllowedApplicationHandler(IAuthorizationHelper authorizationHelper) + => _authorizationHelper = authorizationHelper; + + protected override Task IsAuthorized(AuthorizationHandlerContext context, AllowedApplicationRequirement requirement) + { + IUser user = _authorizationHelper.GetUmbracoUser(context.User); + var allowed = user.AllowedSections.ContainsAny(requirement.Applications); + return Task.FromResult(allowed); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationRequirement.cs new file mode 100644 index 0000000000..dce6d8773e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationRequirement.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorization requirement for the . +/// +internal sealed class AllowedApplicationRequirement : IAuthorizationRequirement +{ + public string[] Applications { get; } + + public AllowedApplicationRequirement(params string[] applications) + => Applications = applications; +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs index 100f3ec3c9..f6673bcd5b 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs @@ -10,5 +10,6 @@ public class DocumentConfigurationResponseModel public required bool AllowNonExistingSegmentsCreation { get; set; } + [Obsolete("Use DocumentTypeConfigurationResponseModel.ReservedFieldNames from the ConfigurationDocumentTypeController endpoint instead.")] public required ISet ReservedFieldNames { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypeConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypeConfigurationResponseModel.cs index c575cc7bc1..802519851a 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypeConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypeConfigurationResponseModel.cs @@ -9,4 +9,6 @@ public class DocumentTypeConfigurationResponseModel public required bool DisableTemplates { get; set; } public required bool UseSegments { get; set; } + + public required ISet ReservedFieldNames { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaConfigurationResponseModel.cs index e1a15250a6..ff723b658f 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaConfigurationResponseModel.cs @@ -6,5 +6,6 @@ public class MediaConfigurationResponseModel public required bool DisableUnpublishWhenReferenced { get; set; } + [Obsolete("Use MediaTypeConfigurationResponseModel.ReservedFieldNames from the ConfigurationMediaTypeController endpoint instead.")] public required ISet ReservedFieldNames { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/MediaTypeConfigurationModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/MediaTypeConfigurationModel.cs new file mode 100644 index 0000000000..313dc7643e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/MediaTypeConfigurationModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.MediaType; + +public class MediaTypeConfigurationResponseModel +{ + public required ISet ReservedFieldNames { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberConfigurationResponseModel.cs index 5faeed7fd8..a3434583bd 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberConfigurationResponseModel.cs @@ -2,5 +2,6 @@ public class MemberConfigurationResponseModel { + [Obsolete("Use MemberTypeConfigurationResponseModel.ReservedFieldNames from the ConfigurationMemberTypeController endpoint instead.")] public required ISet ReservedFieldNames { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeConfigurationResponseModel.cs new file mode 100644 index 0000000000..85e44e93b7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MemberType/MemberTypeConfigurationResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.MemberType; + +public class MemberTypeConfigurationResponseModel +{ + public required ISet ReservedFieldNames { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs new file mode 100644 index 0000000000..8cc71e8482 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.User; + +public class CalculatedUserStartNodesResponseModel +{ + public required Guid Id { get; init; } + + public ISet DocumentStartNodeIds { get; set; } = new HashSet(); + + public bool HasDocumentRootAccess { get; set; } + + public ISet MediaStartNodeIds { get; set; } = new HashSet(); + + public bool HasMediaRootAccess { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrenUserConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrenUserConfigurationResponseModel.cs index 75a6a451d3..fc1d7d8b4f 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrenUserConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrenUserConfigurationResponseModel.cs @@ -6,6 +6,7 @@ public class CurrenUserConfigurationResponseModel { public bool KeepUserLoggedIn { get; set; } + [Obsolete("Use the UserConfigurationResponseModel instead. This will be removed in V15.")] public bool UsernameIsEmail { get; set; } public required PasswordConfigurationResponseModel PasswordConfiguration { get; set; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/UserConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/UserConfigurationResponseModel.cs index f06f137d64..a64cb4273c 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/UserConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/UserConfigurationResponseModel.cs @@ -6,5 +6,7 @@ public class UserConfigurationResponseModel { public bool CanInviteUsers { get; set; } + public bool UsernameIsEmail { get; set; } + public required PasswordConfigurationResponseModel PasswordConfiguration { get; set; } } diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj index 13126a24b5..549ea5cb40 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Index.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Index.cshtml index 453a5ed0fc..16fb9c48e6 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Index.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Index.cshtml @@ -33,7 +33,6 @@ Umbraco - @await Html.BackOfficeImportMapScriptAsync(JsonSerializer, BackOfficePathGenerator, PackageManifestService) diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml index 731ae9f81d..856796cd25 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml @@ -45,7 +45,6 @@ Umbraco -