V14: Public access controller (#14501)
* Implement GetEntryByContentKey * Implement PublicAccessResponseModel * Implement IPublicAccessPresentationFactory * Rename MemberGroupItemReponseModel to MemberGroupItemResponseModel * Refactor PublicAccessResponseModel to use Ids instead of entire content items * Return attempt instead of PresentationModel * Add missing statuses to PublicAccessOperationStatusResult * Implement PublicAccessDocumentController.cs * Refacotr PublicAccessResponseModel to use a base model * Add CreatePublicAccessEntry method * Refactor AccessRequestModel to use names not ids :( * Rename ErrorPageNotFound to ErrorNodeNotFound * Implement new SaveAsync method * Introduce more OperationResults * Implement PublicAccessEntrySlim * Implement SaveAsync * Remove CreatePublicAccessEntry from presentation factory * Rename to CreateAsync * Implement UpdateAsync * Rename to async * Implement CreatePublicAccessEntry * Implement update endpoint * remove PublicAccessEntrySlim mapping * implement CreatePublicAccessEntrySlim method * Refactor UpdateAsync * Remove ContentId from request model as it should be in the request * Use new service layer * Amend method name in update controller * Refactor create public access entry to use async method and return entity * Refactor to use saveAsync method instead of synchronously * Use presentation factory instead of mapping * Implement deleteAsync endpoint * Add produces response type * Refactor mapping to not use UmbracoMapper, as that causes errors * Update OpenApi.json * Refactor out variables to intermediate object * Validate that groups and names are not specified at the same time * Make presentation factory not async * Minor cleanup --------- Co-authored-by: Zeegaan <nge@umbraco.dk> Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch>
This commit is contained in:
@@ -50,4 +50,25 @@ public class ContentControllerBase : ManagementApiControllerBase
|
||||
ContentCreatingOperationStatus.NotFound => NotFound("The content type could not be found"),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."),
|
||||
};
|
||||
|
||||
protected IActionResult PublicAccessOperationStatusResult(PublicAccessOperationStatus status) =>
|
||||
status switch
|
||||
{
|
||||
PublicAccessOperationStatus.ContentNotFound => NotFound("The content could not be found"),
|
||||
PublicAccessOperationStatus.ErrorNodeNotFound => NotFound("The error page could not be found"),
|
||||
PublicAccessOperationStatus.LoginNodeNotFound => NotFound("The login page could not be found"),
|
||||
PublicAccessOperationStatus.NoAllowedEntities => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("No allowed entities given")
|
||||
.WithDetail("Both MemberGroups and Members were empty, thus no entities can be allowed.")
|
||||
.Build()),
|
||||
PublicAccessOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Request cancelled by notification")
|
||||
.WithDetail("The request to save a public access entry was cancelled by a notification handler.")
|
||||
.Build()),
|
||||
PublicAccessOperationStatus.AmbiguousRule => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Ambiguous Rule")
|
||||
.WithDetail("The specified rule is ambiguous, because both member groups and member names were given.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.PublicAccess;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
public class CreatePublicAccessDocumentController : DocumentControllerBase
|
||||
{
|
||||
private readonly IPublicAccessService _publicAccessService;
|
||||
private readonly IPublicAccessPresentationFactory _publicAccessPresentationFactory;
|
||||
|
||||
public CreatePublicAccessDocumentController(
|
||||
IPublicAccessService publicAccessService,
|
||||
IPublicAccessPresentationFactory publicAccessPresentationFactory)
|
||||
{
|
||||
_publicAccessService = publicAccessService;
|
||||
_publicAccessPresentationFactory = publicAccessPresentationFactory;
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpPost("{id:guid}/public-access")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Create(Guid id, PublicAccessRequestModel publicAccessRequestModel)
|
||||
{
|
||||
PublicAccessEntrySlim publicAccessEntrySlim = _publicAccessPresentationFactory.CreatePublicAccessEntrySlim(publicAccessRequestModel, id);
|
||||
|
||||
Attempt<PublicAccessEntry?, PublicAccessOperationStatus> saveAttempt = await _publicAccessService.CreateAsync(publicAccessEntrySlim);
|
||||
|
||||
return saveAttempt.Success
|
||||
? CreatedAtAction<GetPublicAccessDocumentController>(controller => nameof(controller.GetPublicAccess), saveAttempt.Result!.Key)
|
||||
: PublicAccessOperationStatusResult(saveAttempt.Status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
public class DeletePublicAccessDocumentController : DocumentControllerBase
|
||||
{
|
||||
private readonly IPublicAccessService _publicAccessService;
|
||||
|
||||
public DeletePublicAccessDocumentController(IPublicAccessService publicAccessService) => _publicAccessService = publicAccessService;
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpDelete("{id:guid}/public-access")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
await _publicAccessService.DeleteAsync(id);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.PublicAccess;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
public class GetPublicAccessDocumentController : DocumentControllerBase
|
||||
{
|
||||
private readonly IPublicAccessService _publicAccessService;
|
||||
private readonly IPublicAccessPresentationFactory _publicAccessPresentationFactory;
|
||||
|
||||
public GetPublicAccessDocumentController(
|
||||
IPublicAccessService publicAccessService,
|
||||
IPublicAccessPresentationFactory publicAccessPresentationFactory)
|
||||
{
|
||||
_publicAccessService = publicAccessService;
|
||||
_publicAccessPresentationFactory = publicAccessPresentationFactory;
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet("{id:guid}/public-access")]
|
||||
public async Task<IActionResult> GetPublicAccess(Guid id)
|
||||
{
|
||||
Attempt<PublicAccessEntry?, PublicAccessOperationStatus> accessAttempt =
|
||||
await _publicAccessService.GetEntryByContentKeyAsync(id);
|
||||
|
||||
if (accessAttempt.Success is false)
|
||||
{
|
||||
return PublicAccessOperationStatusResult(accessAttempt.Status);
|
||||
}
|
||||
|
||||
Attempt<PublicAccessResponseModel?, PublicAccessOperationStatus> responseModelAttempt =
|
||||
_publicAccessPresentationFactory.CreatePublicAccessResponseModel(accessAttempt.Result!);
|
||||
|
||||
if (responseModelAttempt.Success is false)
|
||||
{
|
||||
return PublicAccessOperationStatusResult(responseModelAttempt.Status);
|
||||
}
|
||||
|
||||
return Ok(responseModelAttempt.Result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.PublicAccess;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
public class UpdatePublicAccessDocumentController : DocumentControllerBase
|
||||
{
|
||||
private readonly IPublicAccessPresentationFactory _publicAccessPresentationFactory;
|
||||
private readonly IPublicAccessService _publicAccessService;
|
||||
|
||||
public UpdatePublicAccessDocumentController(
|
||||
IPublicAccessPresentationFactory publicAccessPresentationFactory,
|
||||
IPublicAccessService publicAccessService)
|
||||
{
|
||||
_publicAccessPresentationFactory = publicAccessPresentationFactory;
|
||||
_publicAccessService = publicAccessService;
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/public-access")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Update(Guid id, PublicAccessRequestModel requestModel)
|
||||
{
|
||||
PublicAccessEntrySlim publicAccessEntrySlim = _publicAccessPresentationFactory.CreatePublicAccessEntrySlim(requestModel, id);
|
||||
|
||||
Attempt<PublicAccessEntry?, PublicAccessOperationStatus> updateAttempt = await _publicAccessService.UpdateAsync(publicAccessEntrySlim);
|
||||
|
||||
return updateAttempt.Success
|
||||
? Ok()
|
||||
: PublicAccessOperationStatusResult(updateAttempt.Status);
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,11 @@ public class ItemMemberGroupItemController : MemberGroupItemControllerBase
|
||||
|
||||
[HttpGet("item")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<MemberGroupItemReponseModel>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(IEnumerable<MemberGroupItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
{
|
||||
IEnumerable<IEntitySlim> memberGroups = _entityService.GetAll(UmbracoObjectTypes.MemberGroup, ids.ToArray());
|
||||
List<MemberGroupItemReponseModel> responseModel = _mapper.MapEnumerable<IEntitySlim, MemberGroupItemReponseModel>(memberGroups);
|
||||
List<MemberGroupItemResponseModel> responseModel = _mapper.MapEnumerable<IEntitySlim, MemberGroupItemResponseModel>(memberGroups);
|
||||
return Ok(responseModel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ internal static class DocumentBuilderExtensions
|
||||
builder.Services.AddTransient<IDocumentNotificationPresentationFactory, DocumentNotificationPresentationFactory>();
|
||||
builder.Services.AddTransient<IContentUrlFactory, ContentUrlFactory>();
|
||||
builder.Services.AddTransient<IDocumentEditingPresentationFactory, DocumentEditingPresentationFactory>();
|
||||
builder.Services.AddTransient<IPublicAccessPresentationFactory, PublicAccessPresentationFactory>();
|
||||
|
||||
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>()
|
||||
.Add<DocumentMapDefinition>()
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.PublicAccess;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
public interface IPublicAccessPresentationFactory
|
||||
{
|
||||
Attempt<PublicAccessResponseModel?, PublicAccessOperationStatus> CreatePublicAccessResponseModel(PublicAccessEntry entry);
|
||||
|
||||
PublicAccessEntrySlim CreatePublicAccessEntrySlim(PublicAccessRequestModel requestModel, Guid contentKey);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberGroup.Item;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.PublicAccess;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
public class PublicAccessPresentationFactory : IPublicAccessPresentationFactory
|
||||
{
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IMemberService _memberService;
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
private readonly IMemberRoleManager _memberRoleManager;
|
||||
|
||||
public PublicAccessPresentationFactory(
|
||||
IEntityService entityService,
|
||||
IMemberService memberService,
|
||||
IUmbracoMapper mapper,
|
||||
IMemberRoleManager memberRoleManager)
|
||||
{
|
||||
_entityService = entityService;
|
||||
_memberService = memberService;
|
||||
_mapper = mapper;
|
||||
_memberRoleManager = memberRoleManager;
|
||||
}
|
||||
|
||||
public Attempt<PublicAccessResponseModel?, PublicAccessOperationStatus> CreatePublicAccessResponseModel(PublicAccessEntry entry)
|
||||
{
|
||||
Attempt<Guid> loginNodeKeyAttempt = _entityService.GetKey(entry.LoginNodeId, UmbracoObjectTypes.Document);
|
||||
Attempt<Guid> noAccessNodeKeyAttempt = _entityService.GetKey(entry.NoAccessNodeId, UmbracoObjectTypes.Document);
|
||||
|
||||
if (loginNodeKeyAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus<PublicAccessResponseModel?, PublicAccessOperationStatus>(PublicAccessOperationStatus.LoginNodeNotFound, null);
|
||||
}
|
||||
|
||||
if (noAccessNodeKeyAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus<PublicAccessResponseModel?, PublicAccessOperationStatus>(PublicAccessOperationStatus.ErrorNodeNotFound, null);
|
||||
}
|
||||
|
||||
// unwrap the current public access setup for the client
|
||||
// - this API method is the single point of entry for both "modes" of public access (single user and role based)
|
||||
var usernames = entry.Rules
|
||||
.Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType)
|
||||
.Select(rule => rule.RuleValue)
|
||||
.ToArray();
|
||||
|
||||
MemberItemResponseModel[] members = usernames
|
||||
.Select(username => _memberService.GetByUsername(username))
|
||||
.Select(_mapper.Map<MemberItemResponseModel>)
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
|
||||
var allGroups = _memberRoleManager.Roles.Where(x => x.Name != null).ToDictionary(x => x.Name!);
|
||||
IEnumerable<UmbracoIdentityRole> identityRoles = entry.Rules
|
||||
.Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType)
|
||||
.Select(rule =>
|
||||
rule.RuleValue is not null && allGroups.TryGetValue(rule.RuleValue, out UmbracoIdentityRole? memberRole)
|
||||
? memberRole
|
||||
: null)
|
||||
.WhereNotNull();
|
||||
|
||||
IEnumerable<IEntitySlim> groupsEntities = _entityService.GetAll(UmbracoObjectTypes.MemberGroup, identityRoles.Select(x => Convert.ToInt32(x.Id)).ToArray());
|
||||
MemberGroupItemResponseModel[] memberGroups = groupsEntities.Select(x => _mapper.Map<MemberGroupItemResponseModel>(x)!).ToArray();
|
||||
|
||||
var responseModel = new PublicAccessResponseModel
|
||||
{
|
||||
Members = members,
|
||||
Groups = memberGroups,
|
||||
LoginPageId = loginNodeKeyAttempt.Result,
|
||||
ErrorPageId = noAccessNodeKeyAttempt.Result,
|
||||
};
|
||||
|
||||
return Attempt.SucceedWithStatus<PublicAccessResponseModel?, PublicAccessOperationStatus>(PublicAccessOperationStatus.Success, responseModel);
|
||||
}
|
||||
|
||||
public PublicAccessEntrySlim CreatePublicAccessEntrySlim(PublicAccessRequestModel requestModel, Guid contentKey) =>
|
||||
new()
|
||||
{
|
||||
ContentId = contentKey,
|
||||
MemberGroupNames = requestModel.MemberGroupNames,
|
||||
MemberUserNames = requestModel.MemberUserNames,
|
||||
ErrorPageId = requestModel.ErrorPageId,
|
||||
LoginPageId = requestModel.LoginPageId,
|
||||
};
|
||||
}
|
||||
@@ -27,7 +27,7 @@ public class ItemTypeMapDefinition : IMapDefinition
|
||||
mapper.Define<IDictionaryItem, DictionaryItemItemResponseModel>((_, _) => new DictionaryItemItemResponseModel(), Map);
|
||||
mapper.Define<IContentType, DocumentTypeItemResponseModel>((_, _) => new DocumentTypeItemResponseModel(), Map);
|
||||
mapper.Define<IMediaType, MediaTypeItemResponseModel>((_, _) => new MediaTypeItemResponseModel(), Map);
|
||||
mapper.Define<IEntitySlim, MemberGroupItemReponseModel>((_, _) => new MemberGroupItemReponseModel(), Map);
|
||||
mapper.Define<IEntitySlim, MemberGroupItemResponseModel>((_, _) => new MemberGroupItemResponseModel(), Map);
|
||||
mapper.Define<ITemplate, TemplateItemResponseModel>((_, _) => new TemplateItemResponseModel { Alias = string.Empty }, Map);
|
||||
mapper.Define<IMemberType, MemberTypeItemResponseModel>((_, _) => new MemberTypeItemResponseModel(), Map);
|
||||
mapper.Define<IRelationType, RelationTypeItemResponseModel>((_, _) => new RelationTypeItemResponseModel(), Map);
|
||||
@@ -77,7 +77,7 @@ public class ItemTypeMapDefinition : IMapDefinition
|
||||
}
|
||||
|
||||
// Umbraco.Code.MapAll
|
||||
private static void Map(IEntitySlim source, MemberGroupItemReponseModel target, MapperContext context)
|
||||
private static void Map(IEntitySlim source, MemberGroupItemResponseModel target, MapperContext context)
|
||||
{
|
||||
target.Name = source.Name ?? string.Empty;
|
||||
target.Id = source.Key;
|
||||
|
||||
@@ -2885,6 +2885,179 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/document/{id}/public-access": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Document"
|
||||
],
|
||||
"operationId": "PostDocumentByIdPublicAccess",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PublicAccessRequestModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PublicAccessRequestModel"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PublicAccessRequestModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"headers": {
|
||||
"Location": {
|
||||
"description": "Location of the newly created resource",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "Location of the newly created resource",
|
||||
"format": "uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"Backoffice User": [ ]
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Document"
|
||||
],
|
||||
"operationId": "DeleteDocumentByIdPublicAccess",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"Backoffice User": [ ]
|
||||
}
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"Document"
|
||||
],
|
||||
"operationId": "GetDocumentByIdPublicAccess",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"Backoffice User": [ ]
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Document"
|
||||
],
|
||||
"operationId": "PutDocumentByIdPublicAccess",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PublicAccessRequestModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PublicAccessRequestModel"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PublicAccessRequestModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"Backoffice User": [ ]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/document/item": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -5817,7 +5990,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemberGroupItemReponseModel"
|
||||
"$ref": "#/components/schemas/MemberGroupItemResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5825,7 +5998,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemberGroupItemReponseModel"
|
||||
"$ref": "#/components/schemas/MemberGroupItemResponseModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5833,7 +6006,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemberGroupItemReponseModel"
|
||||
"$ref": "#/components/schemas/MemberGroupItemResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15074,7 +15247,7 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MemberGroupItemReponseModel": {
|
||||
"MemberGroupItemResponseModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -16214,6 +16387,32 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PublicAccessRequestModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loginPageId": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"errorPageId": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"memberUserNames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"memberGroupNames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PublishedStateModel": {
|
||||
"enum": [
|
||||
"Published",
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.MemberGroup.Item;
|
||||
|
||||
public class MemberGroupItemReponseModel : ItemResponseModelBase
|
||||
public class MemberGroupItemResponseModel : ItemResponseModelBase
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.PublicAccess;
|
||||
|
||||
public class PublicAccessBaseModel
|
||||
{
|
||||
public Guid LoginPageId { get; set; }
|
||||
|
||||
public Guid ErrorPageId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.PublicAccess;
|
||||
|
||||
public class PublicAccessRequestModel : PublicAccessBaseModel
|
||||
{
|
||||
public string[] MemberUserNames { get; set; } = Array.Empty<string>();
|
||||
|
||||
public string[] MemberGroupNames { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberGroup.Item;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.PublicAccess;
|
||||
|
||||
public class PublicAccessResponseModel : PublicAccessBaseModel
|
||||
{
|
||||
public MemberItemResponseModel[] Members { get; set; } = Array.Empty<MemberItemResponseModel>();
|
||||
|
||||
public MemberGroupItemResponseModel[] Groups { get; set; } = Array.Empty<MemberGroupItemResponseModel>();
|
||||
}
|
||||
14
src/Umbraco.Core/Models/PublicAccessEntrySlim.cs
Normal file
14
src/Umbraco.Core/Models/PublicAccessEntrySlim.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
public class PublicAccessEntrySlim
|
||||
{
|
||||
public Guid ContentId { get; set; }
|
||||
|
||||
public string[] MemberUserNames { get; set; } = Array.Empty<string>();
|
||||
|
||||
public string[] MemberGroupNames { get; set; } = Array.Empty<string>();
|
||||
|
||||
public Guid LoginPageId { get; set; }
|
||||
|
||||
public Guid ErrorPageId { get; set; }
|
||||
}
|
||||
11
src/Umbraco.Core/Models/PublicAccessNodesValidationResult.cs
Normal file
11
src/Umbraco.Core/Models/PublicAccessNodesValidationResult.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
public class PublicAccessNodesValidationResult
|
||||
{
|
||||
public IContent? ProtectedNode { get; set; }
|
||||
|
||||
public IContent? LoginNode { get; set; }
|
||||
|
||||
public IContent? ErrorNode { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -61,9 +62,34 @@ public interface IPublicAccessService : IService
|
||||
/// <param name="entry"></param>
|
||||
Attempt<OperationResult?> Save(PublicAccessEntry entry);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the entry asynchronously and returns a status result whether the operation succeeded or not
|
||||
/// </summary>
|
||||
/// <param name="entry"></param>
|
||||
Task<Attempt<PublicAccessEntry?, PublicAccessOperationStatus>> CreateAsync(PublicAccessEntrySlim entry);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the entry asynchronously and returns a status result whether the operation succeeded or not
|
||||
/// </summary>
|
||||
/// <param name="entry"></param>
|
||||
Task<Attempt<PublicAccessEntry?, PublicAccessOperationStatus>> UpdateAsync(PublicAccessEntrySlim entry);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the entry and all associated rules
|
||||
/// </summary>
|
||||
/// <param name="entry"></param>
|
||||
Attempt<OperationResult?> Delete(PublicAccessEntry entry);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entry defined for the content item based on a content key
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns>Returns null if no entry is found</returns>
|
||||
Task<Attempt<PublicAccessEntry?, PublicAccessOperationStatus>> GetEntryByContentKeyAsync(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the entry and all associated rules for a given key.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
Task<Attempt<PublicAccessOperationStatus>> DeleteAsync(Guid key);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
public enum PublicAccessOperationStatus
|
||||
{
|
||||
Success,
|
||||
ContentNotFound,
|
||||
LoginNodeNotFound,
|
||||
ErrorNodeNotFound,
|
||||
NoAllowedEntities,
|
||||
AmbiguousRule,
|
||||
CancelledByNotification,
|
||||
}
|
||||
@@ -2,9 +2,11 @@ using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
@@ -12,14 +14,22 @@ namespace Umbraco.Cms.Core.Services;
|
||||
internal class PublicAccessService : RepositoryService, IPublicAccessService
|
||||
{
|
||||
private readonly IPublicAccessRepository _publicAccessRepository;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IContentService _contentService;
|
||||
|
||||
public PublicAccessService(
|
||||
ICoreScopeProvider provider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IPublicAccessRepository publicAccessRepository)
|
||||
: base(provider, loggerFactory, eventMessagesFactory) =>
|
||||
IPublicAccessRepository publicAccessRepository,
|
||||
IEntityService entityService,
|
||||
IContentService contentService)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
{
|
||||
_publicAccessRepository = publicAccessRepository;
|
||||
_entityService = entityService;
|
||||
_contentService = contentService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all defined entries and associated rules
|
||||
@@ -220,6 +230,112 @@ internal class PublicAccessService : RepositoryService, IPublicAccessService
|
||||
return OperationResult.Attempt.Succeed(evtMsgs);
|
||||
}
|
||||
|
||||
public async Task<Attempt<PublicAccessEntry?, PublicAccessOperationStatus>> CreateAsync(PublicAccessEntrySlim entry)
|
||||
{
|
||||
Attempt<PublicAccessNodesValidationResult, PublicAccessOperationStatus> validationAttempt = ValidatePublicAccessEntrySlim(entry);
|
||||
if (validationAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(validationAttempt.Status, null);
|
||||
}
|
||||
|
||||
IEnumerable<PublicAccessRule> publicAccessRules =
|
||||
entry.MemberUserNames.Any() ? // We only need to check either member usernames or member group names, not both, as we have a check at the top of this method
|
||||
CreateAccessRuleList(entry.MemberUserNames, Constants.Conventions.PublicAccess.MemberUsernameRuleType) :
|
||||
CreateAccessRuleList(entry.MemberGroupNames, Constants.Conventions.PublicAccess.MemberRoleRuleType);
|
||||
|
||||
var publicAccessEntry = new PublicAccessEntry(validationAttempt.Result.ProtectedNode!, validationAttempt.Result.LoginNode!, validationAttempt.Result.ErrorNode!, publicAccessRules);
|
||||
|
||||
Attempt<PublicAccessEntry?, PublicAccessOperationStatus> attempt = await SaveAsync(publicAccessEntry);
|
||||
return attempt.Success ? Attempt.SucceedWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(PublicAccessOperationStatus.Success, attempt.Result!)
|
||||
: Attempt.FailWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(attempt.Status, null);
|
||||
}
|
||||
|
||||
private async Task<Attempt<PublicAccessEntry?, PublicAccessOperationStatus>> SaveAsync(PublicAccessEntry entry)
|
||||
{
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
||||
{
|
||||
var savingNotification = new PublicAccessEntrySavingNotification(entry, eventMessages);
|
||||
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.FailWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(PublicAccessOperationStatus.CancelledByNotification, null);
|
||||
}
|
||||
|
||||
_publicAccessRepository.Save(entry);
|
||||
scope.Complete();
|
||||
|
||||
scope.Notifications.Publish(
|
||||
new PublicAccessEntrySavedNotification(entry, eventMessages).WithStateFrom(savingNotification));
|
||||
}
|
||||
|
||||
return Attempt.SucceedWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(PublicAccessOperationStatus.Success, entry);
|
||||
}
|
||||
|
||||
private Attempt<PublicAccessNodesValidationResult, PublicAccessOperationStatus> ValidatePublicAccessEntrySlim(PublicAccessEntrySlim entry)
|
||||
{
|
||||
var result = new PublicAccessNodesValidationResult();
|
||||
|
||||
if (entry.MemberUserNames.Any() is false && entry.MemberGroupNames.Any() is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(PublicAccessOperationStatus.NoAllowedEntities, result);
|
||||
}
|
||||
|
||||
if(entry.MemberUserNames.Any() && entry.MemberGroupNames.Any())
|
||||
{
|
||||
return Attempt.FailWithStatus(PublicAccessOperationStatus.AmbiguousRule, result);
|
||||
}
|
||||
|
||||
result.ProtectedNode = _contentService.GetById(entry.ContentId);
|
||||
|
||||
if (result.ProtectedNode is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(PublicAccessOperationStatus.ContentNotFound, result);
|
||||
}
|
||||
|
||||
result.LoginNode = _contentService.GetById(entry.LoginPageId);
|
||||
|
||||
if (result.LoginNode is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(PublicAccessOperationStatus.LoginNodeNotFound, result);
|
||||
}
|
||||
|
||||
result.ErrorNode = _contentService.GetById(entry.ErrorPageId);
|
||||
|
||||
if (result.ErrorNode is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(PublicAccessOperationStatus.ErrorNodeNotFound, result);
|
||||
}
|
||||
|
||||
return Attempt.SucceedWithStatus(PublicAccessOperationStatus.Success, result);
|
||||
}
|
||||
|
||||
public async Task<Attempt<PublicAccessEntry?, PublicAccessOperationStatus>> UpdateAsync(PublicAccessEntrySlim entry)
|
||||
{
|
||||
Attempt<PublicAccessNodesValidationResult, PublicAccessOperationStatus> validationAttempt = ValidatePublicAccessEntrySlim(entry);
|
||||
|
||||
if (validationAttempt.Success is false)
|
||||
{
|
||||
return Attempt.FailWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(validationAttempt.Status, null);
|
||||
}
|
||||
|
||||
Attempt<PublicAccessEntry?, PublicAccessOperationStatus> currentPublicAccessEntryAttempt = await GetEntryByContentKeyAsync(entry.ContentId);
|
||||
|
||||
if (currentPublicAccessEntryAttempt.Success is false)
|
||||
{
|
||||
return currentPublicAccessEntryAttempt;
|
||||
}
|
||||
|
||||
PublicAccessEntry mappedEntry = MapToUpdatedEntry(entry, currentPublicAccessEntryAttempt.Result!);
|
||||
|
||||
Attempt<PublicAccessEntry?, PublicAccessOperationStatus> attempt = await SaveAsync(mappedEntry);
|
||||
|
||||
return attempt.Success
|
||||
? Attempt.SucceedWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(PublicAccessOperationStatus.Success, mappedEntry)
|
||||
: Attempt.FailWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(PublicAccessOperationStatus.CancelledByNotification, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the entry and all associated rules
|
||||
/// </summary>
|
||||
@@ -246,4 +362,100 @@ internal class PublicAccessService : RepositoryService, IPublicAccessService
|
||||
|
||||
return OperationResult.Attempt.Succeed(evtMsgs);
|
||||
}
|
||||
|
||||
public Task<Attempt<PublicAccessEntry?, PublicAccessOperationStatus>> GetEntryByContentKeyAsync(Guid key)
|
||||
{
|
||||
IEntitySlim? entity = _entityService.Get(key, UmbracoObjectTypes.Document);
|
||||
if (entity is null)
|
||||
{
|
||||
return Task.FromResult(Attempt.FailWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(PublicAccessOperationStatus.ContentNotFound, null));
|
||||
}
|
||||
|
||||
PublicAccessEntry? entry = GetEntryForContent(entity.Path.EnsureEndsWith("," + entity.Id));
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
return Task.FromResult(Attempt.SucceedWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(PublicAccessOperationStatus.Success, null));
|
||||
}
|
||||
|
||||
return Task.FromResult(Attempt.SucceedWithStatus<PublicAccessEntry?, PublicAccessOperationStatus>(PublicAccessOperationStatus.Success, entry));
|
||||
}
|
||||
|
||||
public async Task<Attempt<PublicAccessOperationStatus>> DeleteAsync(Guid key)
|
||||
{
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
||||
{
|
||||
Attempt<PublicAccessEntry?, PublicAccessOperationStatus> attempt = await GetEntryByContentKeyAsync(key);
|
||||
|
||||
if (attempt.Success is false)
|
||||
{
|
||||
return Attempt.Fail(attempt.Status);
|
||||
}
|
||||
|
||||
EventMessages evtMsgs = EventMessagesFactory.Get();
|
||||
|
||||
|
||||
var deletingNotification = new PublicAccessEntryDeletingNotification(attempt.Result!, evtMsgs);
|
||||
if (scope.Notifications.PublishCancelable(deletingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.Fail(PublicAccessOperationStatus.CancelledByNotification);
|
||||
}
|
||||
|
||||
_publicAccessRepository.Delete(attempt.Result!);
|
||||
|
||||
scope.Complete();
|
||||
|
||||
scope.Notifications.Publish(
|
||||
new PublicAccessEntryDeletedNotification(attempt.Result!, evtMsgs).WithStateFrom(deletingNotification));
|
||||
}
|
||||
|
||||
return Attempt.Succeed(PublicAccessOperationStatus.Success);
|
||||
}
|
||||
|
||||
private IEnumerable<PublicAccessRule> CreateAccessRuleList(string[] ruleValues, string ruleType) =>
|
||||
ruleValues.Select(ruleValue => new PublicAccessRule
|
||||
{
|
||||
RuleValue = ruleValue,
|
||||
RuleType = ruleType,
|
||||
});
|
||||
|
||||
private PublicAccessEntry MapToUpdatedEntry(PublicAccessEntrySlim updatesModel, PublicAccessEntry entryToUpdate)
|
||||
{
|
||||
entryToUpdate.LoginNodeId = _entityService.GetId(updatesModel.LoginPageId, UmbracoObjectTypes.Document).Result;
|
||||
entryToUpdate.NoAccessNodeId = _entityService.GetId(updatesModel.ErrorPageId, UmbracoObjectTypes.Document).Result;
|
||||
|
||||
var isGroupBased = updatesModel.MemberGroupNames.Any();
|
||||
var candidateRuleValues = isGroupBased
|
||||
? updatesModel.MemberGroupNames
|
||||
: updatesModel.MemberUserNames;
|
||||
var newRuleType = isGroupBased
|
||||
? Constants.Conventions.PublicAccess.MemberRoleRuleType
|
||||
: Constants.Conventions.PublicAccess.MemberUsernameRuleType;
|
||||
|
||||
PublicAccessRule[] currentRules = entryToUpdate.Rules.ToArray();
|
||||
IEnumerable<PublicAccessRule> obsoleteRules = currentRules.Where(rule =>
|
||||
rule.RuleType != newRuleType
|
||||
|| candidateRuleValues?.Contains(rule.RuleValue) == false);
|
||||
|
||||
IEnumerable<string>? newRuleValues = candidateRuleValues?.Where(group =>
|
||||
currentRules.Any(rule =>
|
||||
rule.RuleType == newRuleType
|
||||
&& rule.RuleValue == group) == false);
|
||||
|
||||
foreach (PublicAccessRule rule in obsoleteRules)
|
||||
{
|
||||
entryToUpdate.RemoveRule(rule);
|
||||
}
|
||||
|
||||
if (newRuleValues is not null)
|
||||
{
|
||||
foreach (var ruleValue in newRuleValues)
|
||||
{
|
||||
entryToUpdate.AddRule(ruleValue, newRuleType);
|
||||
}
|
||||
}
|
||||
|
||||
return entryToUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user