Browse Source

Allow anonymous access for clients. (#544)

pull/546/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
6c4ea7b7d9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      backend/src/Migrations/OldEvents/AppClientRenamed.cs
  2. 18
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
  3. 19
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  4. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs
  5. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs
  6. 10
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs
  7. 38
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs
  8. 18
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
  9. 16
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  10. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs
  11. 5
      backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
  12. 5
      backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  13. 6
      backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs
  14. 5
      backend/src/Squidex.Infrastructure/StringExtensions.cs
  15. 6
      backend/src/Squidex.Infrastructure/Validation/Not.cs
  16. 2
      backend/src/Squidex.Web/ApiPermissionAttribute.cs
  17. 27
      backend/src/Squidex.Web/Pipeline/AppResolver.cs
  18. 7
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs
  19. 8
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs
  20. 36
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs
  21. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs
  22. 11
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs
  23. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs
  24. 88
      backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs
  25. 19
      frontend/app/features/settings/pages/clients/client.component.html
  26. 4
      frontend/app/features/settings/pages/clients/client.component.ts
  27. 3
      frontend/app/shared/services/clients.service.spec.ts
  28. 7
      frontend/app/shared/services/clients.service.ts

15
backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs → 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<IEvent>
{
public string Id { get; set; }
public string Name { get; set; }
public IEvent Migrate()
{
var result = SimpleMapper.Map(this, new AppClientUpdatedV2());
return result;
}
}
}

18
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);
}
}
}

19
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<AppClients>(id, new AppClient(id, secret, Role.Editor));
return With<AppClients>(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<AppClients>(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<AppClients>(id, client.Update(role));
return With<AppClients>(id, client.Update(name, role, allowAnonymous));
}
}
}

4
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);
}
}
}

6
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;

10
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<string, JsonAppClient>(value.Count);
var json = new Dictionary<string, AppClient>(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<Dictionary<string, JsonAppClient>>(reader)!;
var json = serializer.Deserialize<Dictionary<string, AppClient>>(reader)!;
return new AppClients(json.ToDictionary(x => x.Key, x => x.Value.ToClient()));
return new AppClients(json);
}
}
}

38
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs

@ -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);
}
}
}

18
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) }));

16
backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -35,9 +35,6 @@ namespace Squidex.Domain.Apps.Entities.Apps
AddEventMessage<AppClientUpdated>(
"updated client {[Id]}");
AddEventMessage<AppClientRenamed>(
"renamed client {[Id]} to {[Name]}");
AddEventMessage<AppPlanChanged>(
"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;
}
}
}

4
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; }
}
}

5
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));

5
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));

6
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; }
}
}

5
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;

6
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]))

2
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;

27
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();

7
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.
/// </summary>
[StringLength(20)]
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// The role of the client.
/// </summary>
public string? Role { get; set; }
/// <summary>
/// True to allow anonymous access without an access token for this client.
/// </summary>
public bool? AllowAnonymous { get; set; }
public UpdateClient ToCommand(string clientId)
{
return SimpleMapper.Map(this, new UpdateClient { Id = clientId });

8
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");

36
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);
}

3
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 })
);
}

11
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs

@ -95,17 +95,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
Assert.Throws<DomainObjectNotFoundException>(() => 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()
{

2
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."));
}

88
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<object>()
@ -51,7 +47,6 @@ namespace Squidex.Web.Pipeline
actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object>(), 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<IAppEntity?>(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<UnauthorizedResult>(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<IAppEntity>();
@ -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
{

19
frontend/app/features/settings/pages/clients/client.component.html

@ -64,6 +64,25 @@
</div>
<div class="col-auto cell-actions"></div>
</div>
<div class="form-group row">
<div class="col cell-input offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="{{client.id}}_allowAnonymous"
[disabled]="!client.canUpdate"
[ngModel]="client.allowAnonymous"
(ngModelChange)="updateAccess($event)" />
<label class="form-check-label" for="{{client.id}}_allowAnonymous">
Allow anonymous access.
</label>
</div>
<sqx-form-hint>
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.
</sqx-form-hint>
</div>
<div class="col-auto cell-actions"></div>
</div>
</div>
</div>
</div>

4
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 });
}

3
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);
}

7
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;

Loading…
Cancel
Save