Skip to content

Commit

Permalink
feat: split app-scopes endpoint into, status, login and app-scopes (#…
Browse files Browse the repository at this point in the history
…14075)

Co-authored-by: William Thorenfeldt <48119543+wrt95@users.noreply.github.com>
Co-authored-by: Mirko Sekulic <misha.sekulic@gmail.com>
  • Loading branch information
3 people authored Nov 25, 2024
1 parent 52fc27f commit 7afbaa4
Show file tree
Hide file tree
Showing 18 changed files with 257 additions and 58 deletions.
45 changes: 45 additions & 0 deletions backend/src/Designer/Controllers/AnsattPortenController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Threading.Tasks;
using Altinn.Studio.Designer.Constants;
using Altinn.Studio.Designer.Models.Dto;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.FeatureManagement.Mvc;

namespace Altinn.Studio.Designer.Controllers;

[FeatureGate(StudioFeatureFlags.AnsattPorten)]
[Route("designer/api/[controller]")]
[ApiController]
public class AnsattPortenController : ControllerBase
{
[Authorize(AnsattPortenConstants.AnsattportenAuthorizationPolicy)]
[HttpGet("login")]
public async Task<IActionResult> Login([FromQuery(Name = "redirect_to")] string redirectTo)
{
await Task.CompletedTask;
if (!Url.IsLocalUrl(redirectTo))
{
return Forbid();
}

return LocalRedirect(redirectTo);
}

[AllowAnonymous]
[HttpGet("auth-status")]
public async Task<IActionResult> AuthStatus()
{
await Task.CompletedTask;
var authenticateResult =
await HttpContext.AuthenticateAsync(AnsattPortenConstants.AnsattportenAuthenticationScheme);

var authStatus = new AuthStatus
{
IsLoggedIn = authenticateResult.Succeeded
};

return Ok(authStatus);
}

}
8 changes: 2 additions & 6 deletions backend/src/Designer/Controllers/AppScopesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ namespace Altinn.Studio.Designer.Controllers;
[ApiController]
[FeatureGate(StudioFeatureFlags.AnsattPorten)]
[Route("designer/api/{org}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/app-scopes")]

public class AppScopesController(IMaskinPortenHttpClient maskinPortenHttpClient,
IAppScopesService appScopesService) : ControllerBase
{
Expand All @@ -28,7 +27,7 @@ public async Task<IActionResult> GetScopesFromMaskinPorten(string org, string ap
{
var scopes = await maskinPortenHttpClient.GetAvailableScopes(cancellationToken);

var reponse = new AppScopesResponse()
var response = new AppScopesResponse()
{
Scopes = scopes.Select(x => new MaskinPortenScopeDto()
{
Expand All @@ -37,10 +36,9 @@ public async Task<IActionResult> GetScopesFromMaskinPorten(string org, string ap
}).ToHashSet()
};

return Ok(reponse);
return Ok(response);
}


[Authorize]
[HttpPut]
public async Task UpsertAppScopes(string org, string app, [FromBody] AppScopesUpsertRequest appScopesUpsertRequest,
Expand All @@ -56,7 +54,6 @@ public async Task UpsertAppScopes(string org, string app, [FromBody] AppScopesUp
await appScopesService.UpsertScopesAsync(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer), scopes, cancellationToken);
}


[Authorize]
[HttpGet]
public async Task<IActionResult> GetAppScopes(string org, string app, CancellationToken cancellationToken)
Expand All @@ -74,5 +71,4 @@ public async Task<IActionResult> GetAppScopes(string org, string app, Cancellati

return Ok(reponse);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ private static IServiceCollection AddAnsattPortenAuthentication(this IServiceCol
options.Events.OnRedirectToIdentityProvider = context =>
{

if (!context.Request.Path.StartsWithSegments("/designer/api") ||
!context.Request.Path.Value!.Contains("/maskinporten"))
if (!context.Request.Path.StartsWithSegments("/designer/api/ansattporten/login"))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.HandleResponse();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Configuration;
using Altinn.Studio.Designer.Constants;
using Altinn.Studio.Designer.Helpers;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
Expand Down Expand Up @@ -38,6 +39,7 @@ private static IServiceCollection AddGiteaOidcAuthentication(this IServiceCollec
IConfiguration configuration, IWebHostEnvironment env)
{
var oidcSettings = FetchOidcSettingsFromConfiguration(configuration, env);
bool ansattPortenFeatureFlag = configuration.GetSection($"FeatureManagement:{StudioFeatureFlags.AnsattPorten}").Get<bool>();

services
.AddAuthentication(options =>
Expand All @@ -49,7 +51,8 @@ private static IServiceCollection AddGiteaOidcAuthentication(this IServiceCollec
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SameSite = ansattPortenFeatureFlag ? SameSiteMode.Lax : SameSiteMode.Strict;

options.Cookie.IsEssential = true;

options.ExpireTimeSpan = TimeSpan.FromMinutes(oidcSettings.CookieExpiryTimeInMinutes);
Expand Down
6 changes: 6 additions & 0 deletions backend/src/Designer/Models/Dto/AuthStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Altinn.Studio.Designer.Models.Dto;

public class AuthStatus
{
public bool IsLoggedIn { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models.Dto;
using Designer.Tests.Controllers.AnsattPortenController.Base;
using Designer.Tests.Controllers.ApiTests;
using FluentAssertions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Mvc.Testing.Handlers;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Designer.Tests.Controllers.AnsattPortenController;

public class AuthStatusTest : AnsattPortenControllerTestsBase<AuthStatusTest>, IClassFixture<WebApplicationFactory<Program>>
{
private static string VersionPrefix => "/designer/api/ansattporten/auth-status";

// Setup unauthenticated http client
protected override HttpClient GetTestClient()
{
string configPath = GetConfigPath();
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile(configPath, false, false)
.AddJsonStream(GenerateJsonOverrideConfig())
.AddEnvironmentVariables()
.Build();

return Factory.WithWebHostBuilder(builder =>
{
builder.UseConfiguration(configuration);
builder.ConfigureAppConfiguration((_, conf) =>
{
conf.AddJsonFile(configPath);
conf.AddJsonStream(GenerateJsonOverrideConfig());
});
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureServices(ConfigureTestServicesForSpecificTest);
}).CreateDefaultClient(new CookieContainerHandler());
}

public AuthStatusTest(WebApplicationFactory<Program> factory) : base(factory)
{
}

[Fact]
public async Task AuthStatus_Should_ReturnFalse_IfNotAuthenticated()
{
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, VersionPrefix);

using var response = await HttpClient.SendAsync(httpRequestMessage);
response.StatusCode.Should().Be(HttpStatusCode.OK);

AuthStatus authStatus = await response.Content.ReadAsAsync<AuthStatus>();
authStatus.IsLoggedIn.Should().BeFalse();
}

[Fact]
public async Task AuthStatus_Should_ReturnTrue_IfAuthenticated()
{
// Setup test authentication
ConfigureTestServicesForSpecificTest = services =>
{
services.AddAuthentication(defaultScheme: TestAuthConstants.TestAuthenticationScheme)
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
TestAuthConstants.TestAuthenticationScheme, options => { });
services.AddTransient<IAuthenticationSchemeProvider, TestSchemeProvider>();
};

using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, VersionPrefix);

using var response = await HttpClient.SendAsync(httpRequestMessage);
response.StatusCode.Should().Be(HttpStatusCode.OK);

AuthStatus authStatus = await response.Content.ReadAsAsync<AuthStatus>();
authStatus.IsLoggedIn.Should().BeTrue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Altinn.Studio.Designer.Constants;
using Designer.Tests.Controllers.ApiTests;
using Microsoft.AspNetCore.Mvc.Testing;

namespace Designer.Tests.Controllers.AnsattPortenController.Base;

public class AnsattPortenControllerTestsBase<TControllerTest> : DesignerEndpointsTestsBase<TControllerTest> where TControllerTest : class
{
public AnsattPortenControllerTestsBase(WebApplicationFactory<Program> factory) : base(factory)
{
JsonConfigOverrides.Add(
$$"""
{
"FeatureManagement": {
"{{StudioFeatureFlags.AnsattPorten}}": true
},
"AnsattPortenLoginSettings": {
"ClientId": "non-empty-for-testing",
"ClientSecret": "non-empty-for-testing"
}
}
""");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Constants;
using Designer.Tests.Controllers.AnsattPortenController.Base;
using Designer.Tests.Controllers.ApiTests;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;

namespace Designer.Tests.Controllers.AnsattPortenController;

public class LoginTests : AnsattPortenControllerTestsBase<LoginTests>, IClassFixture<WebApplicationFactory<Program>>
{
private static string VersionPrefix => "/designer/api/ansattporten/login";

public LoginTests(WebApplicationFactory<Program> factory) : base(factory)
{
}

[Theory]
[InlineData("/test", HttpStatusCode.Redirect)]
[InlineData("/", HttpStatusCode.Redirect)]
[InlineData("https://docs.altinn.studio/", HttpStatusCode.Forbidden)]
public async Task LoginShouldReturn_ExpectedCode(string redirectTo, HttpStatusCode expectedStatusCode)
{
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get
, $"{VersionPrefix}?redirect_to={redirectTo}");

using var response = await HttpClient.SendAsync(httpRequestMessage);
Assert.Equal(expectedStatusCode, response.StatusCode);

if (expectedStatusCode == HttpStatusCode.Redirect)
{
Assert.Equal(redirectTo, response.Headers.Location?.ToString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected HttpClient HttpClient
/// </summary>
protected abstract void ConfigureTestServices(IServiceCollection services);

protected Action<IServiceCollection> ConfigureTestForSpecificTest { get; set; } = delegate { };
protected Action<IServiceCollection> ConfigureTestServicesForSpecificTest { get; set; } = delegate { };

/// <summary>
/// Location of the assembly of the executing unit test.
Expand Down Expand Up @@ -97,7 +97,7 @@ protected virtual HttpClient GetTestClient()
TestAuthConstants.TestAuthenticationScheme, options => { });
services.AddTransient<IAuthenticationSchemeProvider, TestSchemeProvider>();
});
builder.ConfigureServices(ConfigureTestForSpecificTest);
builder.ConfigureServices(ConfigureTestServicesForSpecificTest);
}).CreateDefaultClient(new ApiTestsAuthAndCookieDelegatingHandler(), new CookieContainerHandler());
}

Expand Down Expand Up @@ -152,7 +152,7 @@ private void InitializeJsonConfigOverrides()
}


private Stream GenerateJsonOverrideConfig()
protected Stream GenerateJsonOverrideConfig()
{
var overrideJson = Newtonsoft.Json.Linq.JObject.Parse(JsonConfigOverrides.First());
if (JsonConfigOverrides.Count > 1)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Altinn.Studio.Designer.Constants;
using Designer.Tests.Controllers.ApiTests;
using Designer.Tests.Fixtures;
using Microsoft.AspNetCore.Mvc.Testing;
Expand All @@ -9,16 +10,17 @@ public class AppScopesControllerTestsBase<TControllerTest> : DbDesignerEndpoints
{
public AppScopesControllerTestsBase(WebApplicationFactory<Program> factory, DesignerDbFixture designerDbFixture) : base(factory, designerDbFixture)
{
JsonConfigOverrides.Add($@"
{{
""FeatureManagement"": {{
""AnsattPorten"": true
}},
""AnsattPortenLoginSettings"": {{
""ClientId"": ""non-empty-for-testing"",
""ClientSecret"": ""non-empty-for-testing""
}}
}}
");
JsonConfigOverrides.Add(
$$"""
{
"FeatureManagement": {
"{{StudioFeatureFlags.AnsattPorten}}": true
},
"AnsattPortenLoginSettings": {
"ClientId": "non-empty-for-testing",
"ClientSecret": "non-empty-for-testing"
}
}
""");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public async Task Get_Image_Non_Existing_Image_Return_NotFound()
public async Task Call_To_Get_Designer_Iframe_Does_Not_Hit_Image_EndPoint()
{
Mock<IAltinnGitRepositoryFactory> factMock = new();
ConfigureTestForSpecificTest = s =>
ConfigureTestServicesForSpecificTest = s =>
{
s.AddTransient(_ => factMock.Object);
};
Expand Down
2 changes: 1 addition & 1 deletion charts/altinn-designer/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ environmentVariables:
- name: OidcLoginSettings__CookieExpiryTimeInMinutes
value: 59
- name: FeatureManagement__AnsattPorten
value: "false"
value: "true"
- name: FeatureManagement__EidLogging
value: "true"
staging:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { useServicesContext } from 'app-shared/contexts/ServicesContext';

export const useIsLoggedInWithAnsattportenQuery = () => {
const { getIsLoggedInWithAnsattporten } = useServicesContext();
return useQuery<boolean>({
return useQuery<{ isLoggedIn: boolean }>({
queryKey: [QueryKey.IsLoggedInWithAnsattporten],
queryFn: () => getIsLoggedInWithAnsattporten(),
queryFn: getIsLoggedInWithAnsattporten,
});
};
Loading

0 comments on commit 7afbaa4

Please sign in to comment.