diff --git a/src/Squidex.Web/Pipeline/AppResolver.cs b/src/Squidex.Web/Pipeline/AppResolver.cs index bafbbe24c..cc0ae853b 100644 --- a/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/src/Squidex.Web/Pipeline/AppResolver.cs @@ -61,17 +61,6 @@ namespace Squidex.Web.Pipeline (role, permissions) = FindByOpenIdClient(app, user); } - if (permissions == null || permissions.Count == 0) - { - var set = user.Permissions(); - - if (!set.Includes(Permissions.ForApp(Permissions.App, appName)) && !AllowAnonymous(context)) - { - context.Result = new NotFoundResult(); - return; - } - } - if (permissions != null) { var identity = user.Identities.First(); @@ -84,6 +73,14 @@ namespace Squidex.Web.Pipeline } } + var set = user.Permissions(); + + if (!set.Includes(Permissions.ForApp(Permissions.App, appName))&& !AllowAnonymous(context)) + { + context.Result = new NotFoundResult(); + return; + } + context.HttpContext.Features.Set(new AppFeature(app)); } diff --git a/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs new file mode 100644 index 000000000..a1585f58c --- /dev/null +++ b/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -0,0 +1,197 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure.Security; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Xunit; + +#pragma warning disable IDE0017 // Simplify object initialization + +namespace Squidex.Web.Pipeline +{ + public class AppResolverTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly ActionContext actionContext; + private readonly ActionExecutingContext actionExecutingContext; + private readonly ActionExecutionDelegate next; + private readonly ClaimsIdentity user = new ClaimsIdentity(); + private readonly string appName = "my-app"; + private readonly AppResolver sut; + private bool isNextCalled; + + public AppResolverTests() + { + actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor + { + FilterDescriptors = new List() + }); + + actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); + actionExecutingContext.HttpContext = httpContext; + actionExecutingContext.HttpContext.User = new ClaimsPrincipal(user); + actionExecutingContext.RouteData.Values["app"] = appName; + + next = () => + { + isNextCalled = true; + + return Task.FromResult(null); + }; + + sut = new AppResolver(appProvider); + } + + [Fact] + public async Task Should_return_not_found_if_app_not_found() + { + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(Task.FromResult(null)); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_from_user() + { + var app = CreateApp(appName, appUser: "user1"); + + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Features.Get()?.App); + Assert.True(user.Claims.Count() > 2); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_from_client() + { + var app = CreateApp(appName, appClient: "client1"); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Features.Get()?.App); + Assert.True(user.Claims.Count() > 2); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_if_anonymouse_but_not_permissions() + { + var app = CreateApp(appName); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + actionContext.ActionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new AllowAnonymousFilter(), 1)); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Features.Get()?.App); + Assert.Equal(2, user.Claims.Count()); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_return_not_found_if_user_has_no_permissions() + { + var app = CreateApp(appName); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + A.CallTo(() => appProvider.GetAppAsync(appName)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + + [Fact] + public async Task Should_do_nothing_if_parameter_not_set() + { + actionExecutingContext.RouteData.Values.Remove("app"); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => appProvider.GetAppAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + private static IAppEntity CreateApp(string name, string appUser = null, string appClient = null) + { + var app = A.Fake(); + + if (appUser != null) + { + A.CallTo(() => app.Contributors) + .Returns(AppContributors.Empty.Assign(appUser, Role.Owner)); + } + else + { + A.CallTo(() => app.Contributors) + .Returns(AppContributors.Empty); + } + + if (appClient != null) + { + A.CallTo(() => app.Clients) + .Returns(AppClients.Empty.Add(appClient, "secret")); + } + else + { + A.CallTo(() => app.Clients) + .Returns(AppClients.Empty); + } + + A.CallTo(() => app.Roles) + .Returns(Roles.CreateDefaults(name)); + + return app; + } + } +}