diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppClientChanged.cs b/src/Squidex.Domain.Apps.Events/Apps/AppClientChanged.cs new file mode 100644 index 000000000..ed785ff5d --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Apps/AppClientChanged.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// AppClientChanged.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [TypeName("AppClientChangedEvent")] + public sealed class AppClientChanged : AppEvent + { + public string Id { get; set; } + + public bool IsReader { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityClient.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityClient.cs index 830071ecf..ef3305607 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityClient.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityClient.cs @@ -25,6 +25,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps [BsonElement] public string Name { get; set; } + [BsonIgnoreIfDefault] + [BsonElement] + public bool IsReader { get; set; } + string IAppClientEntity.Name { get { return !string.IsNullOrWhiteSpace(Name) ? Name : Id; } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs index 09febaaba..e647e6d61 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs @@ -73,6 +73,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps }); } + protected Task On(AppClientChanged @event, EnvelopeHeaders headers) + { + return Collection.UpdateAsync(@event, headers, a => + { + a.Clients[@event.Id].IsReader = @event.IsReader; + }); + } + protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers) { return Collection.UpdateAsync(@event, headers, a => diff --git a/src/Squidex.Domain.Apps.Read/Apps/IAppClientEntity.cs b/src/Squidex.Domain.Apps.Read/Apps/IAppClientEntity.cs index 8a5e04edf..c3a5ff8e2 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/IAppClientEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/IAppClientEntity.cs @@ -15,5 +15,7 @@ namespace Squidex.Domain.Apps.Read.Apps string Name { get; } string Secret { get; } + + bool IsReader { get; } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppClient.cs b/src/Squidex.Domain.Apps.Write/Apps/AppClient.cs index 5cc9168ad..853a8a402 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppClient.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppClient.cs @@ -6,44 +6,51 @@ // All rights reserved. // ========================================================================== +using System; using Squidex.Infrastructure; +// ReSharper disable InvertIf + namespace Squidex.Domain.Apps.Write.Apps { public sealed class AppClient { private readonly string name; - private readonly string id; private readonly string secret; + private readonly bool isReader; - public string Id + public AppClient(string secret, string name, bool isReader) { - get { return id; } + Guard.NotNullOrEmpty(name, nameof(name)); + Guard.NotNullOrEmpty(secret, nameof(secret)); + + this.name = name; + this.secret = secret; + this.isReader = isReader; } - public string Name + public AppClient Change(bool newIsReader, Func message) { - get { return name ?? Id; } - } + if (isReader == newIsReader) + { + var error = new ValidationError("Client has already the reader state.", "IsReader"); - public string Secret - { - get { return secret; } + throw new ValidationException(message(), error); + } + + return new AppClient(secret, name, newIsReader); } - public AppClient(string id, string secret, string name = null) + public AppClient Rename(string newName, Func message) { - Guard.NotNullOrEmpty(id, nameof(id)); - Guard.NotNullOrEmpty(secret, nameof(secret)); + if (string.Equals(name, newName)) + { + var error = new ValidationError("Client already has the name", "Id"); - this.id = id; - this.name = name; - this.secret = secret; - } + throw new ValidationException(message(), error); + } - public AppClient Rename(string newName) - { - return new AppClient(Id, Secret, newName); + return new AppClient(secret, newName, isReader); } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppClients.cs b/src/Squidex.Domain.Apps.Write/Apps/AppClients.cs index 605b6c9bd..f25159b48 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppClients.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppClients.cs @@ -25,17 +25,23 @@ namespace Squidex.Domain.Apps.Write.Apps public void Add(string id, string secret) { - ThrowIfFound(id, () => "Cannot rename client"); + ThrowIfFound(id, () => "Cannot add client"); - clients[id] = new AppClient(id, secret); + clients[id] = new AppClient(secret, id, false); } public void Rename(string clientId, string name) { ThrowIfNotFound(clientId); - ThrowIfSameName(clientId, name, () => "Cannot rename client"); - clients[clientId] = clients[clientId].Rename(name); + clients[clientId] = clients[clientId].Rename(name, () => "Cannot rename client"); + } + + public void Change(string clientId, bool isReader) + { + ThrowIfNotFound(clientId); + + clients[clientId] = clients[clientId].Change(isReader, () => "Cannot change client"); } public void Revoke(string clientId) @@ -62,15 +68,5 @@ namespace Squidex.Domain.Apps.Write.Apps throw new ValidationException(message(), error); } } - - private void ThrowIfSameName(string clientId, string name, Func message) - { - if (string.Equals(clients[clientId].Name, name)) - { - var error = new ValidationError("Client already has the name", "Id"); - - throw new ValidationException(message(), error); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs b/src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs index b533a84c3..9ccdf4ff9 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs @@ -123,9 +123,9 @@ namespace Squidex.Domain.Apps.Write.Apps return handler.UpdateAsync(context, a => a.RemoveContributor(command)); } - protected Task On(RenameClient command, CommandContext context) + protected Task On(UpdateClient command, CommandContext context) { - return handler.UpdateAsync(context, a => a.RenameClient(command)); + return handler.UpdateAsync(context, a => a.UpdateClient(command)); } protected Task On(RevokeClient command, CommandContext context) diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs index 81263adf9..4415fd533 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Events; @@ -48,11 +47,6 @@ namespace Squidex.Domain.Apps.Write.Apps get { return contributors.Count; } } - public IReadOnlyDictionary Clients - { - get { return clients.Clients; } - } - public AppDomainObject(Guid id, int version) : base(id, version) { @@ -78,6 +72,11 @@ namespace Squidex.Domain.Apps.Write.Apps clients.Add(@event.Id, @event.Secret); } + protected void On(AppClientChanged @event) + { + clients.Change(@event.Id, @event.IsReader); + } + protected void On(AppClientRenamed @event) { clients.Rename(@event.Id, @event.Name); @@ -131,6 +130,25 @@ namespace Squidex.Domain.Apps.Write.Apps return this; } + public AppDomainObject UpdateClient(UpdateClient command) + { + Guard.Valid(command, nameof(command), () => "Cannot rename client"); + + ThrowIfNotCreated(); + + if (!string.IsNullOrWhiteSpace(command.Name)) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); + } + + if (command.IsReader.HasValue) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientChanged { IsReader = command.IsReader.Value })); + } + + return this; + } + public AppDomainObject AssignContributor(AssignContributor command) { Guard.Valid(command, nameof(command), () => "Cannot assign contributor"); @@ -164,17 +182,6 @@ namespace Squidex.Domain.Apps.Write.Apps return this; } - public AppDomainObject RenameClient(RenameClient command) - { - Guard.Valid(command, nameof(command), () => "Cannot rename client"); - - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); - - return this; - } - public AppDomainObject RevokeClient(RevokeClient command) { Guard.Valid(command, nameof(command), () => "Cannot revoke client"); diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/RenameClient.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs similarity index 72% rename from src/Squidex.Domain.Apps.Write/Apps/Commands/RenameClient.cs rename to src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs index 565e572e1..31866abea 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/RenameClient.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs @@ -11,22 +11,24 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Apps.Commands { - public class RenameClient : AppAggregateCommand, IValidatable + public class UpdateClient : AppAggregateCommand, IValidatable { public string Id { get; set; } public string Name { get; set; } + public bool? IsReader { get; set; } + public void Validate(IList errors) { - if (string.IsNullOrWhiteSpace(Name)) + if (!Id.IsSlug()) { - errors.Add(new ValidationError("Name cannot be null or empty", nameof(Name))); + errors.Add(new ValidationError("Client id must be a valid slug", nameof(Id))); } - if (!Id.IsSlug()) + if (string.IsNullOrWhiteSpace(Name) && IsReader == null) { - errors.Add(new ValidationError("Client id must be a valid slug", nameof(Id))); + errors.Add(new ValidationError("Either name or IsReader must be defined.", nameof(Name), nameof(IsReader))); } } } diff --git a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs index 21f8e6721..81c07547f 100644 --- a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs @@ -103,7 +103,7 @@ namespace Squidex.Controllers.Api.Apps [ApiCosts(1)] public async Task PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request) { - await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient { Id = clientId })); + await CommandBus.PublishAsync(SimpleMapper.Map(request, new UpdateClient { Id = clientId })); return NoContent(); } diff --git a/src/Squidex/Controllers/Api/Apps/Models/UpdateAppClientDto.cs b/src/Squidex/Controllers/Api/Apps/Models/UpdateAppClientDto.cs index 69b546ca0..ed129c0b3 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/UpdateAppClientDto.cs +++ b/src/Squidex/Controllers/Api/Apps/Models/UpdateAppClientDto.cs @@ -15,8 +15,12 @@ namespace Squidex.Controllers.Api.Apps.Models /// /// The new display name of the client. /// - [Required] [StringLength(20)] public string Name { get; set; } + + /// + /// Determines if the client is a reader. + /// + public bool IsReader { get; set; } } } diff --git a/src/Squidex/Pipeline/AppApiFilter.cs b/src/Squidex/Pipeline/AppApiFilter.cs index 0f095aafb..61ba12b53 100644 --- a/src/Squidex/Pipeline/AppApiFilter.cs +++ b/src/Squidex/Pipeline/AppApiFilter.cs @@ -81,13 +81,19 @@ namespace Squidex.Pipeline defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppOwner)); defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppDeveloper)); defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppEditor)); + defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppReader)); break; case PermissionLevel.Developer: defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppDeveloper)); defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppEditor)); + defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppReader)); break; case PermissionLevel.Editor: defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppEditor)); + defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppReader)); + break; + case PermissionLevel.Reader: + defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppReader)); break; } @@ -97,20 +103,14 @@ namespace Squidex.Pipeline private static PermissionLevel? FindByOpenIdClient(IAppEntity app, ClaimsPrincipal user) { - var clientId = user.FindFirst(OpenIdClaims.ClientId)?.Value; - - var clientIdParts = clientId?.Split(':'); + var client = app.Clients.FirstOrDefault(x => string.Equals(x.Id, user.GetClientId(), StringComparison.OrdinalIgnoreCase)); - if (clientIdParts?.Length != 2) + if (client == null) { return null; } - clientId = clientIdParts[1]; - - var contributor = app.Clients.FirstOrDefault(x => string.Equals(x.Id, clientId, StringComparison.OrdinalIgnoreCase)); - - return contributor != null ? PermissionLevel.Owner : PermissionLevel.Editor; + return client.IsReader ? PermissionLevel.Reader : PermissionLevel.Editor; } private static PermissionLevel? FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user) diff --git a/src/Squidex/Pipeline/Extensions.cs b/src/Squidex/Pipeline/Extensions.cs index c664ba12d..7ed997a63 100644 --- a/src/Squidex/Pipeline/Extensions.cs +++ b/src/Squidex/Pipeline/Extensions.cs @@ -18,5 +18,21 @@ namespace Squidex.Pipeline { return principal.IsInClient(Constants.FrontendClient); } + + public static string GetClientId(this ClaimsPrincipal principal) + { + var clientId = principal.FindFirst(OpenIdClaims.ClientId)?.Value; + + var clientIdParts = clientId?.Split(':'); + + if (clientIdParts?.Length != 2) + { + return null; + } + + clientId = clientIdParts[1]; + + return clientId; + } } } diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandHandlerTests.cs index b3b4b429a..be3f7a511 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -193,7 +193,7 @@ namespace Squidex.Domain.Apps.Write.Apps CreateApp() .AttachClient(CreateCommand(new AttachClient { Id = clientName })); - var context = CreateContextForCommand(new RenameClient { Id = clientName, Name = "New Name" }); + var context = CreateContextForCommand(new UpdateClient { Id = clientName, Name = "New Name" }); await TestUpdate(app, async _ => { diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs index f5212a608..2b36dcca7 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs @@ -338,66 +338,79 @@ namespace Squidex.Domain.Apps.Write.Apps } [Fact] - public void RenameClient_should_throw_exception_if_not_created() + public void UpdateClient_should_throw_exception_if_not_created() { Assert.Throws(() => { - sut.RenameClient(CreateCommand(new RenameClient { Id = "not-found", Name = clientNewName })); + sut.UpdateClient(CreateCommand(new UpdateClient { Id = "not-found", Name = clientNewName })); }); } [Fact] - public void RenameClient_should_throw_exception_if_command_is_not_valid() + public void UpdateClient_should_throw_exception_if_command_is_not_valid() { CreateApp(); Assert.Throws(() => { - sut.RenameClient(CreateCommand(new RenameClient())); + sut.UpdateClient(CreateCommand(new UpdateClient())); }); Assert.Throws(() => { - sut.RenameClient(CreateCommand(new RenameClient { Id = string.Empty })); + sut.UpdateClient(CreateCommand(new UpdateClient { Id = string.Empty })); }); } [Fact] - public void RenameClient_should_throw_exception_if_client_not_found() + public void UpdateClient_should_throw_exception_if_client_not_found() { CreateApp(); Assert.Throws(() => { - sut.RenameClient(CreateCommand(new RenameClient { Id = "not-found", Name = clientNewName })); + sut.UpdateClient(CreateCommand(new UpdateClient { Id = "not-found", Name = clientNewName })); }); } [Fact] - public void RenameClient_should_throw_exception_if_same_client_name() + public void UpdateClient_should_throw_exception_if_client_has_same_reader_state() { CreateApp(); CreateClient(); - sut.RenameClient(CreateCommand(new RenameClient { Id = clientId, Name = clientNewName })); + Assert.Throws(() => + { + sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, IsReader = false })); + }); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_same_client_name() + { + CreateApp(); + CreateClient(); + + sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName })); Assert.Throws(() => { - sut.RenameClient(CreateCommand(new RenameClient { Id = clientId, Name = clientNewName })); + sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName })); }); } [Fact] - public void RenameClient_should_create_events() + public void UpdateClient_should_create_events() { CreateApp(); CreateClient(); - sut.RenameClient(CreateCommand(new RenameClient { Id = clientId, Name = clientNewName })); + sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName, IsReader = true })); sut.GetUncomittedEvents() .ShouldHaveSameEvents( - CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }) + CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), + CreateEvent(new AppClientChanged { Id = clientId, IsReader = true }) ); }