From 3a23f1b5b939487224321ab0c50d76c662a523a0 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Sun, 24 Nov 2024 00:13:29 +0100 Subject: [PATCH] New page for easily finding the Urls for localtest ApiTokens --- src/Configuration/LocalPlatformSettings.cs | 2 +- src/Controllers/HomeController.cs | 72 +++++++++++++++---- src/Models/TokensModel.cs | 11 +++ .../Implementation/AuthenticationService.cs | 4 +- .../Interface/IAuthentication.cs | 3 +- .../Implementation/ApplicationRepository.cs | 25 +++++-- .../Storage/Implementation/DataRepository.cs | 2 +- src/Views/Home/Tokens.cshtml | 55 ++++++++++++++ src/Views/Shared/_Layout.cshtml | 5 ++ 9 files changed, 154 insertions(+), 25 deletions(-) create mode 100644 src/Models/TokensModel.cs create mode 100644 src/Views/Home/Tokens.cshtml diff --git a/src/Configuration/LocalPlatformSettings.cs b/src/Configuration/LocalPlatformSettings.cs index f0e17bf0..d2c0ea83 100644 --- a/src/Configuration/LocalPlatformSettings.cs +++ b/src/Configuration/LocalPlatformSettings.cs @@ -41,7 +41,7 @@ public string LocalTestingStaticTestDataPath } } - public string LocalFrontendHostname { get; set; } + public string LocalFrontendHostname { get; set; } = "localhost"; public string LocalFrontendProtocol { get; set; } = "http"; diff --git a/src/Controllers/HomeController.cs b/src/Controllers/HomeController.cs index c46c4792..eb8b69d7 100644 --- a/src/Controllers/HomeController.cs +++ b/src/Controllers/HomeController.cs @@ -79,7 +79,7 @@ public async Task Index() model.Org = model.TestApps[0].Value?.Split("/").FirstOrDefault(); model.App = model.TestApps[0].Value?.Split("/").LastOrDefault(); } - model.TestUsers = await GetTestUsersForList(); + model.TestUsers = await GetTestUsersAndPartiesSelectList(); model.UserSelect = Request.Cookies["Localtest_User.Party_Select"]; var defaultAuthLevel = await GetAppAuthLevel(model.AppModeIsHttp, model.TestApps); model.AuthenticationLevels = GetAuthenticationLevels(defaultAuthLevel); @@ -112,6 +112,7 @@ public IActionResult Error() /// /// Method that logs inn test user /// + /// Set to "reauthenticate" if you want to set cookies with no redirect /// An object with information about app and user. /// Redirects to returnUrl [HttpPost] @@ -165,7 +166,7 @@ public async Task LogInTestUser(string action, StartAppModel start using var reader = new StreamReader(prefill.OpenReadStream()); var content = await reader.ReadToEndAsync(); - var token = await _authenticationService.GenerateTokenForOrg(app.Id.Split("/")[0]); + var token = await _authenticationService.GenerateTokenForOrg(app.Id.Split("/")[0], orgNumber: null, authenticationLevel: 3); var newInstance = await _localApp.Instantiate(app.Id, instance, content, xmlDataId, token); return Redirect($"/{app.Id}/#/instance/{newInstance.Id}"); @@ -175,13 +176,30 @@ public async Task LogInTestUser(string action, StartAppModel start return Redirect($"/{app.Id}/"); } + + [AllowAnonymous] + [HttpGet("/Home/Tokens")] + public async Task Tokens() + { + var model = new TokensViewModel + { + AuthenticationLevels = GetAuthenticationLevels(2), + TestUsers = await GetUsersSelectList(), + DefaultOrg = _localPlatformSettings.LocalAppMode == "http" ? (await GetAppsList()).First().Value?.Split("/").FirstOrDefault() : null, + }; + + return View(model); + } + + /// - /// + /// Returns a user token with the given userId as claim /// - /// + /// UserId of the token holder + /// Authentication level of the token /// - [HttpGet("{userId}")] - public async Task GetTestUserToken(int userId) + [HttpGet("/Home/GetTestUserToken/{userId?}")] + public async Task GetTestUserToken(int userId, [FromQuery] int authenticationLevel = 2) { UserProfile profile = await _userProfileService.GetUser(userId); @@ -191,25 +209,36 @@ public async Task GetTestUserToken(int userId) } // Create a test token with long duration - string token = await _authenticationService.GenerateTokenForProfile(profile, 2); + string token = await _authenticationService.GenerateTokenForProfile(profile, authenticationLevel); return Ok(token); } /// /// Returns a org token with the given org as claim /// - /// + /// The short code used to identify the service owner org + /// Organization number to be included in token (if not an official service owner) + /// Authentication level of the token /// - [HttpGet("{id}")] - public async Task GetTestOrgToken(string id, [FromQuery] string orgNumber = null) + [HttpGet("/Home/GetTestOrgToken/{org?}")] + public async Task GetTestOrgToken(string org, [FromQuery] string orgNumber = null, [FromQuery] int? authenticationLevel = 3) + { + // Create a test token with long duration + string token = await _authenticationService.GenerateTokenForOrg(org, orgNumber, authenticationLevel); + + return Ok(token); + } + + [HttpPost("{org}")] + public async Task GetTestOrgTokenPost(string org, [FromQuery] string orgNumber = null, [FromQuery] int? authenticationLevel = 3) { // Create a test token with long duration - string token = await _authenticationService.GenerateTokenForOrg(id, orgNumber); + string token = await _authenticationService.GenerateTokenForOrg(org, orgNumber, authenticationLevel); return Ok(token); } - private async Task> GetTestUsersForList() + private async Task> GetTestUsersAndPartiesSelectList() { var data = await _testDataService.GetTestData(); var userItems = new List(); @@ -252,6 +281,23 @@ private async Task> GetTestUsersForList() return userItems; } + private async Task> GetUsersSelectList() + { + var data = await _testDataService.GetTestData(); + var testUsers = new List(); + foreach (UserProfile profile in data.Profile.User.Values) + { + var properProfile = await _userProfileService.GetUser(profile.UserId); + testUsers.Add(new() + { + Text = properProfile?.Party.Name, + Value = profile.UserId.ToString(), + }); + } + + return testUsers; + } + private async Task GetAppAuthLevel(bool isHttp, IEnumerable testApps) { if (!isHttp) @@ -276,7 +322,7 @@ private async Task GetAppAuthLevel(bool isHttp, IEnumerable } } - private List GetAuthenticationLevels(int defaultAuthLevel) + private static List GetAuthenticationLevels(int defaultAuthLevel) { return new() { diff --git a/src/Models/TokensModel.cs b/src/Models/TokensModel.cs new file mode 100644 index 00000000..1853ad3e --- /dev/null +++ b/src/Models/TokensModel.cs @@ -0,0 +1,11 @@ +#nullable enable +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace LocalTest.Models; + +public class TokensViewModel +{ + public required IEnumerable TestUsers { get; init; } = default!; + public required List AuthenticationLevels { get; init; } = default!; + public required string DefaultOrg { get; init; } +} \ No newline at end of file diff --git a/src/Services/Authentication/Implementation/AuthenticationService.cs b/src/Services/Authentication/Implementation/AuthenticationService.cs index a1711ac4..d26d74aa 100644 --- a/src/Services/Authentication/Implementation/AuthenticationService.cs +++ b/src/Services/Authentication/Implementation/AuthenticationService.cs @@ -56,7 +56,7 @@ public string GenerateToken(ClaimsPrincipal principal) } /// - public async Task GenerateTokenForOrg(string org, string? orgNumber = null) + public async Task GenerateTokenForOrg(string org, string? orgNumber, int? authenticationLevel) { if (orgNumber is null) { @@ -68,7 +68,7 @@ public async Task GenerateTokenForOrg(string org, string? orgNumber = nu string issuer = _generalSettings.Hostname; claims.Add(new Claim(AltinnCoreClaimTypes.Org, org.ToLower(), ClaimValueTypes.String, issuer)); // 3 is the default level for altinn tokens form Maskinporten - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, "3", ClaimValueTypes.Integer32, issuer)); + claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, authenticationLevel.ToString(), ClaimValueTypes.Integer32, issuer)); claims.Add(new Claim("urn:altinn:scope", "altinn:serviceowner/instances.read", ClaimValueTypes.String, issuer)); if (!string.IsNullOrEmpty(orgNumber)) { diff --git a/src/Services/Authentication/Interface/IAuthentication.cs b/src/Services/Authentication/Interface/IAuthentication.cs index ed664234..0ba96f72 100644 --- a/src/Services/Authentication/Interface/IAuthentication.cs +++ b/src/Services/Authentication/Interface/IAuthentication.cs @@ -19,8 +19,9 @@ public interface IAuthentication /// /// Three letter application owner name (eg, TST ) /// Optional Organization number for the application owner. Will be fetched if not provided + /// The authentication level of the generated token /// JWT token - public Task GenerateTokenForOrg(string org, string? orgNumber = null); + public Task GenerateTokenForOrg(string org, string? orgNumber, int? authenticationLevel); /// /// Get JWT token for user profile diff --git a/src/Services/Storage/Implementation/ApplicationRepository.cs b/src/Services/Storage/Implementation/ApplicationRepository.cs index 44265d16..ec76ae68 100644 --- a/src/Services/Storage/Implementation/ApplicationRepository.cs +++ b/src/Services/Storage/Implementation/ApplicationRepository.cs @@ -9,7 +9,7 @@ using Altinn.Platform.Storage.Repository; using LocalTest.Configuration; - +using LocalTest.Services.LocalApp.Interface; using Microsoft.Extensions.Options; namespace LocalTest.Services.Storage.Implementation @@ -17,10 +17,14 @@ namespace LocalTest.Services.Storage.Implementation public class ApplicationRepository : IApplicationRepository { private readonly LocalPlatformSettings _localPlatformSettings; + private readonly ILocalApp _localApp; - public ApplicationRepository(IOptions localPlatformSettings) + public ApplicationRepository( + IOptions localPlatformSettings, + ILocalApp localApp) { _localPlatformSettings = localPlatformSettings.Value; + _localApp = localApp; } public Task Create(Application item) @@ -35,13 +39,20 @@ public Task Delete(string appId, string org) public async Task FindOne(string appId, string org) { - var filename = GetApplicationsDirectory() + appId + ".json"; - if (File.Exists(filename)) + try { - var application = JsonSerializer.Deserialize(await File.ReadAllTextAsync(filename)); - if (application is not null) + // Get fresh first + return await _localApp.GetApplicationMetadata(appId); + } + catch (Exception){ + var filename = GetApplicationsDirectory() + appId + ".json"; + if (File.Exists(filename)) { - return application; + var application = JsonSerializer.Deserialize(await File.ReadAllTextAsync(filename)); + if (application is not null) + { + return application; + } } } diff --git a/src/Services/Storage/Implementation/DataRepository.cs b/src/Services/Storage/Implementation/DataRepository.cs index 60ddabbe..a36d22ef 100644 --- a/src/Services/Storage/Implementation/DataRepository.cs +++ b/src/Services/Storage/Implementation/DataRepository.cs @@ -71,7 +71,7 @@ public async Task> ReadAll(Guid instanceGuid) dataElements.Add(instance); } } - return dataElements.OrderBy(x => x.Created).ToList(); + return dataElements; } public async Task ReadDataFromStorage(string org, string blobStoragePath) diff --git a/src/Views/Home/Tokens.cshtml b/src/Views/Home/Tokens.cshtml new file mode 100644 index 00000000..8ffb460f --- /dev/null +++ b/src/Views/Home/Tokens.cshtml @@ -0,0 +1,55 @@ +@model TokensViewModel +@{ + ViewData["Title"] = "Tokens for localtest"; +} +
+

Welcome to Altinn App Local Testing

+

Create tokens for accessing the localtest apis.

+ +
Note that LocalTest is not an exact replica of the production systems, and that there are differences
+ +

Generate end users token (like from idporten)

+ @using (Html.BeginForm("GetTestUserToken", "Home", FormMethod.Get, new { Class = "form" })) + { +
+ + @Html.DropDownList("userId", Model.TestUsers, new { Class = "form-control" }) +
+
+ + @Html.DropDownList("authenticationLevel", Model.AuthenticationLevels, new { Class = "form-control" }) +
+ +
+ +
+ } +

Service owner tokens

+ + @using (Html.BeginForm("GetTestOrgToken", "Home", FormMethod.Get, new { Class = "form-signin" })) + { +
+ + @Html.TextBox("org", Model.DefaultOrg, new { Class = "form-control" }) +
+
+ + @Html.TextBox("orgNumber", "", new { Class = "form-control", Placeholder = "For official orgs this is fetch from altinncdn.no" }) +
+
+ + @Html.DropDownList("authenticationLevel", Model.AuthenticationLevels, new { Class = "form-control" }) +
+ +
+ +
+ } +
+ +@section Styles +{ + +} \ No newline at end of file diff --git a/src/Views/Shared/_Layout.cshtml b/src/Views/Shared/_Layout.cshtml index 063cfb44..79e7b8db 100644 --- a/src/Views/Shared/_Layout.cshtml +++ b/src/Views/Shared/_Layout.cshtml @@ -6,6 +6,7 @@ @ViewData["Title"] - Altinn Studio + @RenderSection("Styles", required: false)
@@ -29,6 +30,10 @@ User administration +