diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs b/backend/src/Migrations/OldEvents/AppClientRenamed.cs similarity index 55% rename from backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs rename to backend/src/Migrations/OldEvents/AppClientRenamed.cs index dcb22ea51..31f1ad9d0 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs +++ b/backend/src/Migrations/OldEvents/AppClientRenamed.cs @@ -5,15 +5,26 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; +using AppClientUpdatedV2 = Squidex.Domain.Apps.Events.Apps.AppClientUpdated; -namespace Squidex.Domain.Apps.Events.Apps +namespace Migrations.OldEvents { [EventType(nameof(AppClientRenamed))] - public sealed class AppClientRenamed : AppEvent + public sealed class AppClientRenamed : AppEvent, IMigrated { public string Id { get; set; } public string Name { get; set; } + + public IEvent Migrate() + { + var result = SimpleMapper.Map(this, new AppClientUpdatedV2()); + + return result; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs index 687d59849..58e607039 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -17,7 +17,9 @@ namespace Squidex.Domain.Apps.Core.Apps public string Secret { get; } - public AppClient(string name, string secret, string role) + public bool AllowAnonymous { get; set; } + + public AppClient(string name, string secret, string role, bool allowAnonymous) : base(name) { Guard.NotNullOrEmpty(secret, nameof(secret)); @@ -26,22 +28,14 @@ namespace Squidex.Domain.Apps.Core.Apps Role = role; Secret = secret; - } - - [Pure] - public AppClient Update(string newRole) - { - Guard.NotNullOrEmpty(newRole, nameof(newRole)); - return new AppClient(Name, Secret, newRole); + AllowAnonymous = allowAnonymous; } [Pure] - public AppClient Rename(string newName) + public AppClient Update(string? name, string? role, bool? allowAnonymous) { - Guard.NotNullOrEmpty(newName, nameof(newName)); - - return new AppClient(newName, Secret, Role); + return new AppClient(name.Or(Name), Secret, role.Or(Role), allowAnonymous ?? AllowAnonymous); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs index e8caa2f28..cb6e38679 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -53,11 +53,11 @@ namespace Squidex.Domain.Apps.Core.Apps throw new ArgumentException("Id already exists.", nameof(id)); } - return With(id, new AppClient(id, secret, Role.Editor)); + return With(id, new AppClient(id, secret, Role.Editor, false)); } [Pure] - public AppClients Rename(string id, string newName) + public AppClients Update(string id, string? name = null, string? role = null, bool? allowAnonymous = false) { Guard.NotNullOrEmpty(id, nameof(id)); @@ -66,20 +66,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return With(id, client.Rename(newName)); - } - - [Pure] - public AppClients Update(string id, string role) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - if (!TryGetValue(id, out var client)) - { - return this; - } - - return With(id, client.Update(role)); + return With(id, client.Update(name, role, allowAnonymous)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs index a4c10d824..7deb3f55b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs @@ -28,9 +28,9 @@ namespace Squidex.Domain.Apps.Core.Apps } [Pure] - public AppPattern Update(string newName, string newPattern, string? newMessage) + public AppPattern Update(string? name, string? pattern, string? message) { - return new AppPattern(newName, newPattern, newMessage); + return new AppPattern(name.Or(Name), pattern.Or(Pattern), message); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs index 294d1ccc0..606765773 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; -using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; namespace Squidex.Domain.Apps.Core.Apps @@ -41,11 +40,8 @@ namespace Squidex.Domain.Apps.Core.Apps } [Pure] - public AppPatterns Update(Guid id, string name, string pattern, string? message = null) + public AppPatterns Update(Guid id, string? name = null, string? pattern = null, string? message = null) { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNullOrEmpty(pattern, nameof(pattern)); - if (!TryGetValue(id, out var appPattern)) { return this; diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs index a03bba8d5..217705ea3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs @@ -17,11 +17,11 @@ namespace Squidex.Domain.Apps.Core.Apps.Json { protected override void WriteValue(JsonWriter writer, AppClients value, JsonSerializer serializer) { - var json = new Dictionary(value.Count); + var json = new Dictionary(value.Count); - foreach (var (key, appClient) in value) + foreach (var (key, client) in value) { - json.Add(key, new JsonAppClient(appClient)); + json.Add(key, client); } serializer.Serialize(writer, json); @@ -29,9 +29,9 @@ namespace Squidex.Domain.Apps.Core.Apps.Json protected override AppClients ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { - var json = serializer.Deserialize>(reader)!; + var json = serializer.Deserialize>(reader)!; - return new AppClients(json.ToDictionary(x => x.Key, x => x.Value.ToClient())); + return new AppClients(json); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs deleted file mode 100644 index a3380c03c..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Core.Apps.Json -{ - public class JsonAppClient - { - [JsonProperty] - public string Name { get; set; } - - [JsonProperty] - public string Secret { get; set; } - - [JsonProperty] - public string Role { get; set; } - - public JsonAppClient() - { - } - - public JsonAppClient(AppClient client) - { - SimpleMapper.Map(client, this); - } - - public AppClient ToClient() - { - return new AppClient(Name, Secret, Role); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index 0da6580e0..f26b10311 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -341,19 +341,6 @@ namespace Squidex.Domain.Apps.Entities.Apps } } - public void UpdateClient(UpdateClient command) - { - if (!string.IsNullOrWhiteSpace(command.Name)) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); - } - - if (command.Role != null) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Role = command.Role })); - } - } - public void ChangePlan(ChangePlan command) { if (string.Equals(appPlansProvider.GetFreePlan()?.Id, command.PlanId)) @@ -371,6 +358,11 @@ namespace Squidex.Domain.Apps.Entities.Apps RaiseEvent(SimpleMapper.Map(command, new AppUpdated())); } + public void UpdateClient(UpdateClient command) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated())); + } + public void UploadImage(UploadAppImage command) { RaiseEvent(SimpleMapper.Map(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) })); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index ed1487a9b..39eaf5c36 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -35,9 +35,6 @@ namespace Squidex.Domain.Apps.Entities.Apps AddEventMessage( "updated client {[Id]}"); - AddEventMessage( - "renamed client {[Id]} to {[Name]}"); - AddEventMessage( "changed plan to {[Plan]}"); @@ -85,8 +82,8 @@ namespace Squidex.Domain.Apps.Entities.Apps return CreateContributorsEvent(e, e.ContributorId); case AppClientAttached e: return CreateClientsEvent(e, e.Id); - case AppClientRenamed e: - return CreateClientsEvent(e, e.Id, ClientName(e)); + case AppClientUpdated e: + return CreateClientsEvent(e, e.Id); case AppClientRevoked e: return CreateClientsEvent(e, e.Id); case AppLanguageAdded e: @@ -138,9 +135,9 @@ namespace Squidex.Domain.Apps.Entities.Apps return ForEvent(e, "settings.patterns").Param("PatternId", id).Param("Name", name); } - private HistoryEvent CreateClientsEvent(IEvent e, string id, string? name = null) + private HistoryEvent CreateClientsEvent(IEvent e, string id) { - return ForEvent(e, "settings.clients").Param("Id", id).Param("Name", name); + return ForEvent(e, "settings.clients").Param("Id", id); } private HistoryEvent CreatePlansEvent(IEvent e, string? plan = null) @@ -152,10 +149,5 @@ namespace Squidex.Domain.Apps.Entities.Apps { return Task.FromResult(CreateEvent(@event.Payload)); } - - private static string ClientName(AppClientRenamed e) - { - return !string.IsNullOrWhiteSpace(e.Name) ? e.Name : e.Id; - } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs index 854242be3..474db16b0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs @@ -11,8 +11,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { public string Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } public string? Role { get; set; } + + public bool? AllowAnonymous { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs index a3828ffaf..1bebbfac7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs @@ -59,11 +59,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards e(Not.Defined("Client id"), nameof(command.Id)); } - if (string.IsNullOrWhiteSpace(command.Name) && command.Role == null) - { - e(Not.DefinedOr("name", "role"), nameof(command.Name), nameof(command.Role)); - } - if (command.Role != null && !roles.Contains(command.Role)) { e(Not.Valid("role"), nameof(command.Role)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 742608a9b..7c7d15332 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -85,10 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State return UpdateClients(e, (ev, c) => c.Add(ev.Id, ev.Secret)); case AppClientUpdated e: - return UpdateClients(e, (ev, c) => c.Update(ev.Id, ev.Role)); - - case AppClientRenamed e: - return UpdateClients(e, (ev, c) => c.Rename(ev.Id, ev.Name)); + return UpdateClients(e, (ev, c) => c.Update(ev.Id, ev.Name, ev.Role, ev.AllowAnonymous)); case AppClientRevoked e: return UpdateClients(e, (ev, c) => c.Revoke(ev.Id)); diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs index e09e4f6cb..519c02e26 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs @@ -14,6 +14,10 @@ namespace Squidex.Domain.Apps.Events.Apps { public string Id { get; set; } - public string Role { get; set; } + public string? Name { get; set; } + + public string? Role { get; set; } + + public bool? AllowAnonymous { get; set; } } } diff --git a/backend/src/Squidex.Infrastructure/StringExtensions.cs b/backend/src/Squidex.Infrastructure/StringExtensions.cs index a3acbab4e..e89ce28fd 100644 --- a/backend/src/Squidex.Infrastructure/StringExtensions.cs +++ b/backend/src/Squidex.Infrastructure/StringExtensions.cs @@ -542,6 +542,11 @@ namespace Squidex.Infrastructure return value != null && PropertyNameRegex.IsMatch(value); } + public static string Or(this string? value, string fallback) + { + return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; + } + public static string WithFallback(this string? value, string fallback) { return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; diff --git a/backend/src/Squidex.Infrastructure/Validation/Not.cs b/backend/src/Squidex.Infrastructure/Validation/Not.cs index f10a78a69..a24767ca0 100644 --- a/backend/src/Squidex.Infrastructure/Validation/Not.cs +++ b/backend/src/Squidex.Infrastructure/Validation/Not.cs @@ -71,12 +71,6 @@ namespace Squidex.Infrastructure.Validation return $"{Upper(property)} is not a valid value."; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string DefinedOr(string property1, string property2) - { - return $"Either {Lower(property1)} or {Lower(property2)} must be defined."; - } - private static string Lower(string property) { if (char.IsUpper(property[0])) diff --git a/backend/src/Squidex.Web/ApiPermissionAttribute.cs b/backend/src/Squidex.Web/ApiPermissionAttribute.cs index edcee1427..c48fd41d8 100644 --- a/backend/src/Squidex.Web/ApiPermissionAttribute.cs +++ b/backend/src/Squidex.Web/ApiPermissionAttribute.cs @@ -15,7 +15,7 @@ using Squidex.Shared; namespace Squidex.Web { - public sealed class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter + public sealed class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter, IAllowAnonymous { private readonly string[] permissionIds; diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs index bd6ce35ad..54c8a92ff 100644 --- a/backend/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -58,6 +58,11 @@ namespace Squidex.Web.Pipeline (role, permissions) = FindByOpenIdClient(app, user); } + if (permissions == null) + { + (role, permissions) = FindAnonymousClient(app); + } + if (permissions != null) { var identity = user.Identities.First(); @@ -77,7 +82,15 @@ namespace Squidex.Web.Pipeline if (!AllowAnonymous(context) && !HasPermission(appName, requestContext)) { - context.Result = new NotFoundResult(); + if (string.IsNullOrWhiteSpace(user.Identity.AuthenticationType)) + { + context.Result = new UnauthorizedResult(); + } + else + { + context.Result = new NotFoundResult(); + } + return; } @@ -136,6 +149,18 @@ namespace Squidex.Web.Pipeline return (null, null); } + private static (string?, PermissionSet?) FindAnonymousClient(IAppEntity app) + { + var client = app.Clients.Values.FirstOrDefault(x => x.AllowAnonymous); + + if (client != null && app.Roles.TryGet(app.Name, client.Role, out var role)) + { + return (client.Role, role.Permissions); + } + + return (null, null); + } + private static (string?, PermissionSet?) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user) { var subjectId = user.OpenIdSubject(); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs index 44e949983..9f7cd207f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs @@ -17,13 +17,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// The new display name of the client. /// [StringLength(20)] - public string Name { get; set; } + public string? Name { get; set; } /// /// The role of the client. /// public string? Role { get; set; } + /// + /// True to allow anonymous access without an access token for this client. + /// + public bool? AllowAnonymous { get; set; } + public UpdateClient ToCommand(string clientId) { return SimpleMapper.Map(this, new UpdateClient { Id = clientId }); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs index 5a2ac24a7..ec187aed6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs @@ -23,10 +23,12 @@ namespace Squidex.Domain.Apps.Core.Model.Apps clients = clients.Add("3", "my-secret"); clients = clients.Add("4", "my-secret"); - clients = clients.Update("3", Role.Editor); + clients = clients.Update("3", role: Role.Editor); - clients = clients.Rename("3", "My Client 3"); - clients = clients.Rename("2", "My Client 2"); + clients = clients.Update("3", name: "My Client 3"); + clients = clients.Update("2", name: "My Client 2"); + + clients = clients.Update("1", allowAnonymous: true); clients = clients.Revoke("4"); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs index 1e3ba2b96..252284ebb 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs @@ -23,15 +23,15 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var clients_1 = clients_0.Add("2", "my-secret"); - clients_1["2"].Should().BeEquivalentTo(new AppClient("2", "my-secret", Role.Editor)); + clients_1["2"].Should().BeEquivalentTo(new AppClient("2", "my-secret", Role.Editor, false)); } [Fact] public void Should_assign_clients_with_permission() { - var clients_1 = clients_0.Add("2", new AppClient("my-name", "my-secret", Role.Reader)); + var clients_1 = clients_0.Add("2", new AppClient("my-name", "my-secret", Role.Reader, false)); - clients_1["2"].Should().BeEquivalentTo(new AppClient("my-name", "my-secret", Role.Reader)); + clients_1["2"].Should().BeEquivalentTo(new AppClient("my-name", "my-secret", Role.Reader, false)); } [Fact] @@ -47,45 +47,37 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var clients_1 = clients_0.Add("2", "my-secret"); - clients_1.Add("2", new AppClient("my-name", "my-secret", "my-role")); + clients_1.Add("2", new AppClient("my-name", "my-secret", "my-role", false)); } [Fact] - public void Should_rename_client() + public void Should_update_client_with_role() { - var clients_1 = clients_0.Rename("1", "new-name"); + var client_1 = clients_0.Update("1", role: Role.Reader); - clients_1["1"].Should().BeEquivalentTo(new AppClient("new-name", "my-secret", Role.Editor)); + client_1["1"].Should().BeEquivalentTo(new AppClient("1", "my-secret", Role.Reader, false)); } [Fact] - public void Should_return_same_clients_if_client_is_updated_with_the_same_values() + public void Should_update_client_with_name() { - var clients_1 = clients_0.Rename("2", "2"); + var client_1 = clients_0.Update("1", name: "New-Name"); - Assert.Same(clients_0, clients_1); - } - - [Fact] - public void Should_return_same_clients_if_client_to_rename_not_found() - { - var clients_1 = clients_0.Rename("2", "new-name"); - - Assert.Same(clients_0, clients_1); + client_1["1"].Should().BeEquivalentTo(new AppClient("New-Name", "my-secret", Role.Editor, false)); } [Fact] - public void Should_update_client() + public void Should_update_client_with_allow_anonymous() { - var client_1 = clients_0.Update("1", Role.Reader); + var client_1 = clients_0.Update("1", allowAnonymous: true); - client_1["1"].Should().BeEquivalentTo(new AppClient("1", "my-secret", Role.Reader)); + client_1["1"].Should().BeEquivalentTo(new AppClient("1", "my-secret", Role.Editor, true)); } [Fact] public void Should_return_same_clients_if_client_to_update_not_found() { - var clients_1 = clients_0.Update("2", Role.Reader); + var clients_1 = clients_0.Update("2", role: Role.Reader); Assert.Same(clients_0, clients_1); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index e6b0e5794..e9d17f017 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -406,8 +406,7 @@ namespace Squidex.Domain.Apps.Entities.Apps LastEvents .ShouldHaveSameEvents( - CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), - CreateEvent(new AppClientUpdated { Id = clientId, Role = Role.Developer }) + CreateEvent(new AppClientUpdated { Id = clientId, Name = clientNewName, Role = Role.Developer }) ); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs index 80bf6162f..64d478b6a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs @@ -95,17 +95,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards Assert.Throws(() => GuardAppClients.CanUpdate(clients_0, command, Roles.Empty)); } - [Fact] - public void UpdateClient_should_throw_exception_if_client_has_no_name_and_role() - { - var command = new UpdateClient { Id = "ios" }; - - var clients_1 = clients_0.Add("ios", "secret"); - - ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command, roles), - new ValidationError("Either name or role must be defined.", "Name", "Role")); - } - [Fact] public void UpdateClient_should_throw_exception_if_client_has_invalid_role() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs index ed688e8f4..59cb5572e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs @@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards var command = new DeleteRole { Name = roleName }; - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients.Add("1", new AppClient("client", "1", roleName))), + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients.Add("1", new AppClient("client", "1", roleName, false))), new ValidationError("Cannot remove a role when a client is assigned.")); } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs index 2b25a85b0..b0c06d516 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -34,16 +34,12 @@ namespace Squidex.Web.Pipeline private readonly ActionContext actionContext; private readonly ActionExecutingContext actionExecutingContext; private readonly ActionExecutionDelegate next; - private readonly ClaimsIdentity userIdentiy = new ClaimsIdentity(); - private readonly ClaimsPrincipal user; private readonly string appName = "my-app"; private readonly AppResolver sut; private bool isNextCalled; public AppResolverTests() { - user = new ClaimsPrincipal(userIdentiy); - actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor { EndpointMetadata = new List() @@ -51,7 +47,6 @@ namespace Squidex.Web.Pipeline actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); actionExecutingContext.HttpContext = httpContext; - actionExecutingContext.HttpContext.User = user; actionExecutingContext.RouteData.Values["app"] = appName; next = () => @@ -67,6 +62,8 @@ namespace Squidex.Web.Pipeline [Fact] public async Task Should_return_not_found_if_app_not_found() { + SetupUser(); + A.CallTo(() => appProvider.GetAppAsync(appName, false)) .Returns(Task.FromResult(null)); @@ -76,13 +73,31 @@ namespace Squidex.Web.Pipeline Assert.False(isNextCalled); } + [Fact] + public async Task Should_return_401_if_user_is_anonymous() + { + var user = SetupUser(null); + + var app = CreateApp(appName); + + A.CallTo(() => appProvider.GetAppAsync(appName, false)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + [Fact] public async Task Should_resolve_app_from_user() { + var user = SetupUser(); + var app = CreateApp(appName, appUser: "user1"); - userIdentiy.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - userIdentiy.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); A.CallTo(() => appProvider.GetAppAsync(appName, true)) .Returns(app); @@ -97,9 +112,28 @@ namespace Squidex.Web.Pipeline [Fact] public async Task Should_resolve_app_from_client() { + var user = SetupUser(); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + var app = CreateApp(appName, appClient: "client1"); - userIdentiy.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + A.CallTo(() => appProvider.GetAppAsync(appName, true)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Count() > 2); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_from_anonymous_client() + { + var user = SetupUser(); + + var app = CreateApp(appName, appClient: "client1", allowAnonymous: true); A.CallTo(() => appProvider.GetAppAsync(appName, true)) .Returns(app); @@ -112,12 +146,14 @@ namespace Squidex.Web.Pipeline } [Fact] - public async Task Should_resolve_app_if_anonymous_but_not_permissions() + public async Task Should_resolve_app_if_action_allows_anonymous_but_user_has_no_permissions() { - var app = CreateApp(appName); + var user = SetupUser(); - userIdentiy.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); - userIdentiy.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + var app = CreateApp(appName); actionContext.ActionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); @@ -134,10 +170,12 @@ namespace Squidex.Web.Pipeline [Fact] public async Task Should_return_not_found_if_user_has_no_permissions() { - var app = CreateApp(appName); + var user = SetupUser(); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - userIdentiy.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); - userIdentiy.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + var app = CreateApp(appName); A.CallTo(() => appProvider.GetAppAsync(appName, false)) .Returns(app); @@ -151,9 +189,11 @@ namespace Squidex.Web.Pipeline [Fact] public async Task Should_return_not_found_if_client_is_from_another_app() { - var app = CreateApp(appName, appClient: "client1"); + var user = SetupUser(); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "other:client1")); - userIdentiy.AddClaim(new Claim(OpenIdClaims.ClientId, "other:client1")); + var app = CreateApp(appName, appClient: "client1"); A.CallTo(() => appProvider.GetAppAsync(appName, false)) .Returns(app); @@ -177,7 +217,17 @@ namespace Squidex.Web.Pipeline .MustNotHaveHappened(); } - private static IAppEntity CreateApp(string name, string? appUser = null, string? appClient = null) + private ClaimsIdentity SetupUser(string? type = "OIDC") + { + var userIdentity = new ClaimsIdentity(type); + var userPrincipal = new ClaimsPrincipal(userIdentity); + + actionExecutingContext.HttpContext.User = userPrincipal; + + return userIdentity; + } + + private static IAppEntity CreateApp(string name, string? appUser = null, string? appClient = null, bool? allowAnonymous = null) { var appEntity = A.Fake(); @@ -195,7 +245,7 @@ namespace Squidex.Web.Pipeline if (appClient != null) { A.CallTo(() => appEntity.Clients) - .Returns(AppClients.Empty.Add(appClient, "secret")); + .Returns(AppClients.Empty.Add(appClient, "secret").Update(appClient, allowAnonymous: allowAnonymous)); } else { diff --git a/frontend/app/features/settings/pages/clients/client.component.html b/frontend/app/features/settings/pages/clients/client.component.html index 5e9869ac5..491cabef6 100644 --- a/frontend/app/features/settings/pages/clients/client.component.html +++ b/frontend/app/features/settings/pages/clients/client.component.html @@ -64,6 +64,25 @@
+
+
+
+ + + +
+ + + Allow access to the API without an access token to all resources that are configured via the role of this client. Do not give more than one client anonymous access. + +
+
+
diff --git a/frontend/app/features/settings/pages/clients/client.component.ts b/frontend/app/features/settings/pages/clients/client.component.ts index 044dba6c6..1b8cb4cfb 100644 --- a/frontend/app/features/settings/pages/clients/client.component.ts +++ b/frontend/app/features/settings/pages/clients/client.component.ts @@ -37,6 +37,10 @@ export class ClientComponent { this.clientsState.update(this.client, { role }); } + public updateAccess(allowAnonymous: boolean) { + this.clientsState.update(this.client, { allowAnonymous }); + } + public rename(name: string) { this.clientsState.update(this.client, { name }); } diff --git a/frontend/app/shared/services/clients.service.spec.ts b/frontend/app/shared/services/clients.service.spec.ts index 6c43c546a..6ef1eac31 100644 --- a/frontend/app/shared/services/clients.service.spec.ts +++ b/frontend/app/shared/services/clients.service.spec.ts @@ -165,6 +165,7 @@ describe('ClientsService', () => { name: `Client ${id}`, role: `Role${id}`, secret: `secret${id}`, + allowAnonymous: true, _links: { update: { method: 'PUT', href: `/clients/id${id}` } } @@ -191,5 +192,5 @@ export function createClient(id: number) { update: { method: 'PUT', href: `/clients/id${id}` } }; - return new ClientDto(links, `id${id}`, `Client ${id}`, `secret${id}`, `Role${id}`); + return new ClientDto(links, `id${id}`, `Client ${id}`, `secret${id}`, `Role${id}`, true); } \ No newline at end of file diff --git a/frontend/app/shared/services/clients.service.ts b/frontend/app/shared/services/clients.service.ts index e90051fdc..2f065386a 100644 --- a/frontend/app/shared/services/clients.service.ts +++ b/frontend/app/shared/services/clients.service.ts @@ -29,7 +29,8 @@ export class ClientDto { public readonly id: string, public readonly name: string, public readonly secret: string, - public readonly role: string + public readonly role: string, + public readonly allowAnonymous: boolean ) { this._links = links; @@ -53,6 +54,7 @@ export interface CreateClientDto { export interface UpdateClientDto { readonly name?: string; readonly role?: string; + readonly allowAnonymous?: boolean; } @Injectable() @@ -144,7 +146,8 @@ function parseClients(response: any): ClientsPayload { item.id, item.name || item.id, item.secret, - item.role)); + item.role, + item.allowAnonymous)); const _links = response._links;