diff --git a/backend/src/Designer/Configuration/CacheSettings.cs b/backend/src/Designer/Configuration/CacheSettings.cs index 1311725ea03..d04b9d7a1aa 100644 --- a/backend/src/Designer/Configuration/CacheSettings.cs +++ b/backend/src/Designer/Configuration/CacheSettings.cs @@ -3,5 +3,7 @@ public class CacheSettings { public int DataNorgeApiCacheTimeout { get; set; } + + public int OrgListCacheTimeout { get; set; } } } diff --git a/backend/src/Designer/Controllers/ResourceAdminController.cs b/backend/src/Designer/Controllers/ResourceAdminController.cs index 57a590f8a65..d6774b4684a 100644 --- a/backend/src/Designer/Controllers/ResourceAdminController.cs +++ b/backend/src/Designer/Controllers/ResourceAdminController.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Altinn.Authorization.ABAC.Xacml; using Altinn.ResourceRegistry.Core.Enums.Altinn2; +using Altinn.ResourceRegistry.Core.Models; using Altinn.ResourceRegistry.Core.Models.Altinn2; using Altinn.Studio.Designer.Configuration; using Altinn.Studio.Designer.Helpers; @@ -30,8 +31,9 @@ public class ResourceAdminController : ControllerBase private readonly IMemoryCache _memoryCache; private readonly CacheSettings _cacheSettings; private readonly IAltinn2MetadataClient _altinn2MetadataClient; + private readonly IOrgService _orgService; - public ResourceAdminController(IGitea gitea, IRepository repository, IResourceRegistryOptions resourceRegistryOptions, IMemoryCache memoryCache, IOptions cacheSettings, IAltinn2MetadataClient altinn2MetadataClient) + public ResourceAdminController(IGitea gitea, IRepository repository, IResourceRegistryOptions resourceRegistryOptions, IMemoryCache memoryCache, IOptions cacheSettings, IAltinn2MetadataClient altinn2MetadataClient, IOrgService orgService) { _giteaApi = gitea; _repository = repository; @@ -39,6 +41,7 @@ public ResourceAdminController(IGitea gitea, IRepository repository, IResourceRe _memoryCache = memoryCache; _cacheSettings = cacheSettings.Value; _altinn2MetadataClient = altinn2MetadataClient; + _orgService = orgService; } [HttpGet] @@ -158,15 +161,17 @@ public ActionResult GetValidateResource(string org, string repository, string id [HttpPut] [Route("designer/api/{org}/resources/updateresource/{id}")] - public ActionResult UpdateResource(string org, string id, [FromBody] ServiceResource resource) + public async Task UpdateResource(string org, string id, [FromBody] ServiceResource resource) { + resource.HasCompetentAuthority = await GetCompetentAuthorityFromOrg(org); return _repository.UpdateServiceResource(org, id, resource); } [HttpPost] [Route("designer/api/{org}/resources/addresource")] - public ActionResult AddResource(string org, [FromBody] ServiceResource resource) + public async Task> AddResource(string org, [FromBody] ServiceResource resource) { + resource.HasCompetentAuthority = await GetCompetentAuthorityFromOrg(org); return _repository.AddServiceResource(org, resource); } @@ -333,5 +338,44 @@ public async Task PublishResource(string org, string repository, s return new StatusCodeResult(400); } } + + private async Task GetCompetentAuthorityFromOrg(string org) + { + Org organization = await GetOrg(org); + if (organization == null) + { + return null; + } + return new CompetentAuthority() { Name = organization.Name, Organization = organization.Orgnr, Orgcode = org }; + } + + private async Task GetOrg(string org) + { + OrgList orgList = await GetOrgList(); + + if (orgList.Orgs.TryGetValue(org, out Org organization)) + { + return organization; + } + + return null; + } + + private async Task GetOrgList() + { + string cacheKey = "orglist"; + if (!_memoryCache.TryGetValue(cacheKey, out OrgList orgList)) + { + orgList = await _orgService.GetOrgList(); + + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetPriority(CacheItemPriority.High) + .SetAbsoluteExpiration(new TimeSpan(0, _cacheSettings.OrgListCacheTimeout, 0)); + + _memoryCache.Set(cacheKey, orgList, cacheEntryOptions); + } + + return orgList; + } } } diff --git a/backend/src/Designer/Designer.csproj b/backend/src/Designer/Designer.csproj index d6d2e59997c..99cecebde26 100644 --- a/backend/src/Designer/Designer.csproj +++ b/backend/src/Designer/Designer.csproj @@ -94,6 +94,8 @@ + + @@ -103,10 +105,12 @@ + + diff --git a/backend/src/Designer/Enums/ResourcePartyType.cs b/backend/src/Designer/Enums/ResourcePartyType.cs index cf52edf2427..a9936e1dcfe 100644 --- a/backend/src/Designer/Enums/ResourcePartyType.cs +++ b/backend/src/Designer/Enums/ResourcePartyType.cs @@ -1,7 +1,9 @@ using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace Altinn.Studio.Designer.Enums { + [JsonConverter(typeof(JsonStringEnumConverter))] public enum ResourcePartyType { [EnumMember(Value = "PrivatePerson")] diff --git a/backend/src/Designer/Infrastructure/ServiceRegistration.cs b/backend/src/Designer/Infrastructure/ServiceRegistration.cs index 02eb251b090..15cc4d495f1 100644 --- a/backend/src/Designer/Infrastructure/ServiceRegistration.cs +++ b/backend/src/Designer/Infrastructure/ServiceRegistration.cs @@ -52,6 +52,7 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddHttpClient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/backend/src/Designer/Models/Org.cs b/backend/src/Designer/Models/Org.cs new file mode 100644 index 00000000000..1d910b7f13f --- /dev/null +++ b/backend/src/Designer/Models/Org.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Altinn.ResourceRegistry.Core.Models +{ + /// + /// Describes an organization + /// + public class Org + { + /// + /// Name of organization. With lanugage support + /// + [JsonProperty("name")] + public Dictionary Name { get; set; } + + /// + /// The logo + /// + public string Logo { get; set; } + + /// + /// The organization number + /// + public string Orgnr { get; set; } + + /// + /// The homepage + /// + public string Homepage { get; set; } + + /// + /// The environments available for the organzation + /// + public List Environments { get; set; } + } +} diff --git a/backend/src/Designer/Models/OrgList.cs b/backend/src/Designer/Models/OrgList.cs new file mode 100644 index 00000000000..00de4f9db5f --- /dev/null +++ b/backend/src/Designer/Models/OrgList.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Altinn.ResourceRegistry.Core.Models; +using Newtonsoft.Json; + +namespace Altinn.Studio.Designer.Models +{ + /// + /// Defines a list of orgs + /// + public class OrgList + { + /// + /// Dictionary of orgs + /// + public Dictionary Orgs { get; set; } + } +} diff --git a/backend/src/Designer/Services/Implementation/OrgService.cs b/backend/src/Designer/Services/Implementation/OrgService.cs new file mode 100644 index 00000000000..09a73d71f9c --- /dev/null +++ b/backend/src/Designer/Services/Implementation/OrgService.cs @@ -0,0 +1,38 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Altinn.Studio.Designer.Configuration; +using Altinn.Studio.Designer.Models; +using Altinn.Studio.Designer.Services.Interfaces; + +namespace Altinn.Studio.Designer.Services.Implementation +{ + /// + /// Client responsible for collection + /// + public class OrgService : IOrgService + { + private readonly HttpClient _client; + private readonly GeneralSettings _generalSettings; + + /// + /// Default constructor + /// + public OrgService(HttpClient client, GeneralSettings generalSettingsOptions) + { + _client = client; + _generalSettings = generalSettingsOptions; + } + + /// + /// Returns configured org list + /// + public async Task GetOrgList() + { + HttpResponseMessage response = await _client.GetAsync(_generalSettings.OrganizationsUrl); + response.EnsureSuccessStatusCode(); + string orgListString = await response.Content.ReadAsStringAsync(); + OrgList orgList = System.Text.Json.JsonSerializer.Deserialize(orgListString, new System.Text.Json.JsonSerializerOptions() { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase }); + return orgList; + } + } +} diff --git a/backend/src/Designer/Services/Implementation/RepositorySI.cs b/backend/src/Designer/Services/Implementation/RepositorySI.cs index b80051b72ae..fa5228e09fb 100644 --- a/backend/src/Designer/Services/Implementation/RepositorySI.cs +++ b/backend/src/Designer/Services/Implementation/RepositorySI.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using System.Xml; using Altinn.Authorization.ABAC.Utils; @@ -40,6 +41,7 @@ public class RepositorySI : IRepository private readonly IApplicationMetadataService _applicationMetadataService; private readonly ITextsService _textsService; private readonly IResourceRegistry _resourceRegistryService; + private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions() { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, WriteIndented = true }; /// /// Initializes a new instance of the class @@ -762,7 +764,7 @@ public List GetServiceResources(string org, string repository, foreach (FileSystemObject resourceFile in resourceFiles) { string jsonString = File.ReadAllText($"{repopath}/{resourceFile.Path}"); - ServiceResource serviceResource = JsonConvert.DeserializeObject(jsonString); + ServiceResource serviceResource = System.Text.Json.JsonSerializer.Deserialize(jsonString, _serializerOptions); if (serviceResource != null) { @@ -784,11 +786,11 @@ public ActionResult UpdateServiceResource(string org, string id, ServiceResource foreach (FileSystemObject resourceFile in resourceFiles) { string jsonString = File.ReadAllText($"{repopath}/{resourceFile.Path}"); - ServiceResource serviceResource = JsonConvert.DeserializeObject(jsonString); + ServiceResource serviceResource = System.Text.Json.JsonSerializer.Deserialize(jsonString, _serializerOptions); if (serviceResource != null && serviceResource.Identifier == updatedResource.Identifier) { - string updatedResourceString = JsonConvert.SerializeObject(updatedResource); + string updatedResourceString = System.Text.Json.JsonSerializer.Serialize(updatedResource, _serializerOptions); File.WriteAllText($"{repopath}/{resourceFile.Path}", updatedResourceString); return new StatusCodeResult(201); } @@ -811,7 +813,7 @@ public ActionResult AddServiceResource(string org, ServiceResource newResource) { string repopath = _settings.GetServicePath(org, repository, AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext)); string fullPathOfNewResource = Path.Combine(repopath, newResource.Identifier.AsFileName(), string.Format("{0}_resource.json", newResource.Identifier)); - string newResourceJson = JsonConvert.SerializeObject(newResource); + string newResourceJson = System.Text.Json.JsonSerializer.Serialize(newResource, _serializerOptions); Directory.CreateDirectory(Path.Combine(repopath, newResource.Identifier.AsFileName())); File.WriteAllText(fullPathOfNewResource, newResourceJson); diff --git a/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs b/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs index 4a85ea34dff..0151bc415ac 100644 --- a/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs +++ b/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Altinn.ApiClients.Maskinporten.Interfaces; using Altinn.ApiClients.Maskinporten.Models; @@ -10,9 +11,11 @@ using Altinn.Studio.Designer.Helpers; using Altinn.Studio.Designer.Models; using Altinn.Studio.Designer.Services.Interfaces; +using Altinn.Studio.Designer.TypedHttpClients.AzureDevOps.Enums; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Options; -using Newtonsoft.Json; namespace Altinn.Studio.Designer.Services.Implementation { @@ -25,6 +28,7 @@ public class ResourceRegistryService : IResourceRegistry private readonly PlatformSettings _platformSettings; private readonly ResourceRegistryIntegrationSettings _resourceRegistrySettings; private readonly ResourceRegistryMaskinportenIntegrationSettings _maskinportenIntegrationSettings; + private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions() { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, WriteIndented = true }; public ResourceRegistryService() { @@ -71,10 +75,35 @@ public async Task PublishServiceResource(ServiceResource serviceRe fullWritePolicyToResourceRegistryUrl = $"{_platformSettings.ResourceRegistryDefaultBaseUrl}{_platformSettings.ResourceRegistryUrl}/{serviceResource.Identifier}/policy"; } - string serviceResourceString = JsonConvert.SerializeObject(serviceResource); + string serviceResourceString = System.Text.Json.JsonSerializer.Serialize(serviceResource, _serializerOptions); _httpClientFactory.CreateClient("myHttpClient"); _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken); + HttpResponseMessage getResourceResponse = await _httpClient.GetAsync(getResourceRegistryUrl); + + HttpResponseMessage response; + + if (getResourceResponse.IsSuccessStatusCode && getResourceResponse.StatusCode.Equals(HttpStatusCode.OK)) + { + string putRequest = $"{publishResourceToResourceRegistryUrl}/{serviceResource.Identifier}"; + using (StringContent putContent = new StringContent(serviceResourceString, Encoding.UTF8, "application/json")) + { + response = await _httpClient.PutAsync(putRequest, putContent); + } + } + else + { + using (StringContent postContent = new StringContent(serviceResourceString, Encoding.UTF8, "application/json")) + { + response = await _httpClient.PostAsync(publishResourceToResourceRegistryUrl, postContent); + } + } + + if (!response.IsSuccessStatusCode) + { + return await GetPublishResponse(response); + } + if (policyPath != null) { MultipartFormDataContent content = new MultipartFormDataContent(); @@ -125,18 +154,7 @@ public async Task PublishServiceResource(ServiceResource serviceRe } } - HttpResponseMessage getResourceResponse = await _httpClient.GetAsync(getResourceRegistryUrl); - - if (getResourceResponse.IsSuccessStatusCode) - { - string putRequest = $"{publishResourceToResourceRegistryUrl}/{serviceResource.Identifier}"; - HttpResponseMessage putResponse = await _httpClient.PutAsync(putRequest, new StringContent(serviceResourceString, Encoding.UTF8, "application/json")); - return putResponse.IsSuccessStatusCode ? new StatusCodeResult(201) : new StatusCodeResult(400); - } - - HttpResponseMessage response = await _httpClient.PostAsync(publishResourceToResourceRegistryUrl, new StringContent(serviceResourceString, Encoding.UTF8, "application/json")); - - return GetPublishResponse(response); + return await GetPublishResponse(response); } private async Task GetBearerTokenFromMaskinporten() @@ -144,7 +162,7 @@ private async Task GetBearerTokenFromMaskinporten() return await _maskinPortenService.GetToken(_maskinportenClientDefinition.ClientSettings.EncodedJwk, _maskinportenClientDefinition.ClientSettings.Environment, _maskinportenClientDefinition.ClientSettings.ClientId, _maskinportenClientDefinition.ClientSettings.Scope, _maskinportenClientDefinition.ClientSettings.Resource, _maskinportenClientDefinition.ClientSettings.ConsumerOrgNo); } - private StatusCodeResult GetPublishResponse(HttpResponseMessage response) + private async Task GetPublishResponse(HttpResponseMessage response) { if (response.StatusCode == HttpStatusCode.Created) { @@ -156,7 +174,16 @@ private StatusCodeResult GetPublishResponse(HttpResponseMessage response) } else { - return new StatusCodeResult(400); + string responseContent = await response.Content.ReadAsStringAsync(); + try + { + ProblemDetails problems = JsonSerializer.Deserialize(responseContent); + return new ObjectResult(problems) { StatusCode = (int)response.StatusCode }; + } + catch (Exception) + { + return new ContentResult() { Content = responseContent, StatusCode = (int)response.StatusCode }; + } } } diff --git a/backend/src/Designer/Services/Interfaces/IOrgService.cs b/backend/src/Designer/Services/Interfaces/IOrgService.cs new file mode 100644 index 00000000000..234e97b3d4b --- /dev/null +++ b/backend/src/Designer/Services/Interfaces/IOrgService.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Altinn.ResourceRegistry.Core.Models; +using Altinn.Studio.Designer.Models; + +namespace Altinn.Studio.Designer.Services.Interfaces +{ + /// + /// Interface to describe the org service + /// + public interface IOrgService + { + /// + /// Returns a list of orga + /// + /// + public Task GetOrgList(); + } +} diff --git a/backend/src/Designer/appsettings.json b/backend/src/Designer/appsettings.json index 74053a75a8e..6f3106f9e55 100644 --- a/backend/src/Designer/appsettings.json +++ b/backend/src/Designer/appsettings.json @@ -22,7 +22,8 @@ "ResourceRegistryDefaultBaseUrl": "http://localhost:5100" }, "CacheSettings": { - "DataNorgeApiCacheTimeout": 3600 + "DataNorgeApiCacheTimeout": 3600, + "OrgListCacheTimeout": 3600 }, "GeneralSettings": { "HostName": "studio.localhost",