diff --git a/backend/PolicyAdmin/Models/PolicyRule.cs b/backend/PolicyAdmin/Models/PolicyRule.cs index 5e3c3e08c54..b38cd7c04c1 100644 --- a/backend/PolicyAdmin/Models/PolicyRule.cs +++ b/backend/PolicyAdmin/Models/PolicyRule.cs @@ -8,6 +8,8 @@ public class PolicyRule public List? Subject { get; set; } + public List? AccessPackages { get; set; } + public List? Actions { get; set; } public List>? Resources { get; set; } diff --git a/backend/PolicyAdmin/PolicyConverter.cs b/backend/PolicyAdmin/PolicyConverter.cs index c049c0ab471..5575d7cc7e1 100644 --- a/backend/PolicyAdmin/PolicyConverter.cs +++ b/backend/PolicyAdmin/PolicyConverter.cs @@ -21,6 +21,7 @@ public static ResourcePolicy ConvertPolicy(XacmlPolicy xacmlPolicy) rule.Description = xr.Description; rule.Subject = new List(); + rule.AccessPackages = new List(); rule.Actions = new List(); rule.Resources = new List>(); @@ -31,7 +32,9 @@ public static ResourcePolicy ConvertPolicy(XacmlPolicy xacmlPolicy) { foreach (XacmlAllOf allOf in anyOf.AllOf) { - List? subject = GetRuleSubjects(allOf); + List? subject = GetRuleSubjects(allOf)?.Where(x => !x.StartsWith("urn:altinn:accesspackage")).ToList(); + + List? accessPackages = GetRuleSubjects(allOf)?.Where(x => x.StartsWith("urn:altinn:accesspackage")).ToList(); List? resource = GetRuleResources(allOf); @@ -42,6 +45,11 @@ public static ResourcePolicy ConvertPolicy(XacmlPolicy xacmlPolicy) rule.Subject.AddRange(subject); } + if (accessPackages != null) + { + rule.AccessPackages.AddRange(accessPackages); + } + if (action != null) { rule.Actions.AddRange(action); @@ -195,6 +203,11 @@ private static XacmlRule ConvertRule(PolicyRule policyRule) ruleAnyOfs.Add(GetSubjectAnyOfs(policyRule.Subject)); } + if (policyRule.AccessPackages != null && policyRule.AccessPackages.Count > 0) + { + ruleAnyOfs.Add(GetSubjectAnyOfs(policyRule.AccessPackages)); + } + if (policyRule.Resources != null && policyRule.Resources.Count > 0) { ruleAnyOfs.Add(GetResourceAnyOfs(policyRule.Resources)); diff --git a/backend/src/Designer/Controllers/ResourceAdminController.cs b/backend/src/Designer/Controllers/ResourceAdminController.cs index 47eead00ed1..0eebd87625e 100644 --- a/backend/src/Designer/Controllers/ResourceAdminController.cs +++ b/backend/src/Designer/Controllers/ResourceAdminController.cs @@ -433,6 +433,218 @@ public async Task>> GetEuroVoc(CancellationToken return sectors; } + [HttpGet] + [Route("designer/api/{org}/resources/accesspackages")] + public async Task GetAccessPackages(string org, CancellationToken cancellationToken) + { + // 1. GET accesspackages (mocked for now) + AccessPackageArea transportArea = new() + { + Id = "589217CF-6070-474F-9989-8C5359C740F4", + Name = "Transport og lagring", + Description = "Denne tilgangspakken er relevant for alle tjenester rettet mot virksomheter med aktivitet innen transport og lagring", + ShortDescription = "Tjenester rettet mot virksomheter med aktivitet innen transport og lagring", + IconName = "TruckIcon" + }; + AccessPackageArea skattArea = new() + { + Id = "F52ADD40-6748-4E89-875A-63D3E153605D", + Name = "Skatt og Merverdiavgift", + Description = "Denne tilgangspakken er relevant for alle virksomheter som betaler eller rapporterer inn informasjon knyttet til skatt, avgift regnskap og toll", + ShortDescription = "Tjenester som angår skatt, avgift, regnskap og toll", + IconName = "BankNoteIcon" + }; + AccessPackageArea jordbrukArea = new() + { + Id = "tag_jordbruk", + IconName = "PlantIcon", + ShortDescription = "Tjenester rettet mot virksomheter med aktivitet innen jordbruk, skogbruk, jakt, fiske og akvakultur", + Name = "Jordbruk, skogbruk, jakt, fiske og akvakultur", + Description = "Denne tilgangspakken er relevant for tjenester rettet mot virksomheter med aktivitet innen jordbruk, skogbruk, jakt, fiske og akvakultur.", + }; + AccessPackageArea revisorArea = new() + { + Id = "tag_regnskapsforer", + IconName = "ReceiptIcon", + ShortDescription = "Tjenester som det er naturlig at en regnskapsfører utfører på vegne av sine kunder", + Name = "Regnskapsførere", + Description = "Denne fullmakten gir tilgang til alle tjenester som det er naturlig at en regnskapsfører utfører på vegne av sine kunder", + }; + + List accessPackages = + [ + new() + { + Id = "urn:altinn:accesspackage:foretaksskatt", + Urn = "urn:altinn:accesspackage:foretaksskatt", + Name = "Foretaksskatt", + Description = "Denne tilgangspakken gir fullmakter til tjenester knyttet til skatt for foretak.", + Tags = [], + Area = skattArea + }, + new() + { + Id = "urn:altinn:accesspackage:skattegrunnlag", + Urn = "urn:altinn:accesspackage:skattegrunnlag", + Name = "Skattegrunnlag", + Description = "Denne tilgangspakken gir fullmakter til tjenester knyttet til innhenting av skattegrunnlag.", + Tags = [], + Area = skattArea + }, + new() + { + Id = "urn:altinn:accesspackage:merverdiavgift", + Urn = "urn:altinn:accesspackage:merverdiavgift", + Name = "Merverdiavgift", + Description = "Denne tilgangspakken gir fullmakter til tjenester knyttet til merverdiavgift.", + Tags = [], + Area = skattArea + }, + new() + { + Id = "urn:altinn:accesspackage:sjofart", + Urn = "urn:altinn:accesspackage:sjofart", + Name = "Sjøfart", + Description = "Denne fullmakten gir tilgang til alle tjenester knyttet til skipsarbeidstakere og fartøy til sjøs.", + Tags = [], + Area = transportArea + }, + new() + { + Id = "urn:altinn:accesspackage:lufttransport", + Urn = "urn:altinn:accesspackage:lufttransport", + Name = "Lufttransport", + Description = "Denne fullmakten gir tilgang til alle tjenester knyttet til luftfartøy og romfartøy.", + Tags = [], + Area = transportArea + }, + new() + { + Id = "urn:altinn:accesspackage:jordbruk", + Urn = "urn:altinn:accesspackage:jordbruk", + Name = "Jordbruk", + Description = "Denne tilgangspakken gir tilgang til tjenester knyttet til jordbruk. Ved regelverksendringer eller innføring av nye digitale tjenester kan det bli endringer i tilganger som fullmakten gir", + Tags = [], + Area = jordbrukArea + }, + new() + { + Id = "urn:altinn:accesspackage:fiske", + Urn = "urn:altinn:accesspackage:fiske", + Name = "Fiske", + Description = "Denne tilgangspakken gir fullmakter til tjenester knyttet til fiske. Ved regelverksendringer eller innføring av nye digitale tjenester kan det bli endringer i tilganger som fullmakten gir", + Tags = [], + Area = jordbrukArea + }, + new() + { + Id = "urn:altinn:accesspackage:regnskapsforermedsigneringsrettighet", + Urn = "urn:altinn:accesspackage:regnskapsforermedsigneringsrettighet", + Name = "Regnskapsfører med signeringsrettighet", + Description = "Denne fullmakten gir tilgang til regnskapfører å kunne signere på vegne av kunden for alle tjenester som krever signeringsrett. Dette er tjenester som man har vurdert det som naturlig at en regnskapsfører utfører på vegne av sin kunde. Fullmakten gis kun til autoriserte regnskapsførere. Fullmakt hos regnskapfører oppstår når kunden registrerer regnskapsfører i Enhetsregisteret. Ved regelverksendringer eller innføring av nye digitale tjenester kan det bli endringer i tilganger som fullmakten gir.", + Tags = [], + Area = revisorArea + }, + new() + { + Id = "urn:altinn:accesspackage:regnskapsforerutensigneringsrettighet", + Urn = "urn:altinn:accesspackage:regnskapsforerutensigneringsrettighet", + Name = "Regnskapsfører uten signeringsrettighet", + Description = "Denne fullmakten gir tilgang til å kunne utføre alle tjenester som ikke krever signeringsrett. Dette er tjenester som man har vurdert det som naturlig at en regnskapsfører utfører på vegne av sin kunde. Fullmakten gis kun til autoriserte regnskapsførere. Fullmakt hos regnskapfører oppstår når kunden registrerer regnskapsfører i Enhetsregisteret. Ved regelverksendringer eller innføring av nye digitale tjenester kan det bli endringer i tilganger som fullmakten gir.", + Tags = [], + Area = revisorArea + }, + new() + { + Id = "urn:altinn:accesspackage:regnskapsforerlonn", + Urn = "urn:altinn:accesspackage:regnskapsforerlonn", + Name = "Regnskapsfører lønn", + Description = "Denne fullmakten gir tilgang til regnskapsfører å rapportere lønn for sin kunde. Dette er tjenester som man har vurdert det som naturlig at en regnskapsfører utfører på vegne av sin kunde. Fullmakten gis kun til autoriserte regnskapsførere. Fullmakt hos regnskapfører oppstår når kunden registrerer regnskapsfører i Enhetsregisteret. Ved regelverksendringer eller innføring av nye digitale tjenester kan det bli endringer i tilganger som fullmakten gir.", + Tags = [], + Area = revisorArea + } + ]; + + string env = "tt02"; + IEnumerable subjects = accessPackages.Select(accessPackage => accessPackage.Urn); + + OrgList orgList = await GetOrgList(); + + // 2. POST to get all resources per access package + List subjectResources = await _resourceRegistry.GetSubjectResources(subjects.ToList(), env); + subjectResources.Add(new SubjectResources() + { + Subject = new AttributeMatchV2() + { + Type = "", + Value = "", + Urn = "urn:altinn:accesspackage:regnskapsforerlonn" + }, + Resources = new List() { + new AttributeMatchV2() { + Type = "", + Value = "brg-maskinportenschemaid-5", + Urn = "" + }, + new AttributeMatchV2() { + Type = "", + Value = "mat-maskinportenschemaid-54", + Urn = "" + }, + new AttributeMatchV2() { + Type = "", + Value = "skd-maskinportenschemaid-32", + Urn = "" + }, + new AttributeMatchV2() { + Type = "", + Value = "slk-maskinportenschemaid-81", + Urn = "" + }, + new AttributeMatchV2() { + Type = "", + Value = "nav-maskinportenschemaid-141", + Urn = "" + }, + new AttributeMatchV2() { + Type = "", + Value = "svv-maskinportenschemaid-249", + Urn = "" + } + } + }); + + // 3. GET full list of resources + List environmentResources = await _resourceRegistry.GetResourceList(env, false); + + // 4. map resource to access package based on data from step 2. + accessPackages.ForEach(accessPackage => + { + List resources = subjectResources.Find(x => x.Subject.Urn == accessPackage.Urn)?.Resources; + + resources?.ForEach(resourceMatch => + { + ServiceResource fullResource = environmentResources.Find(x => x.Identifier == resourceMatch.Value); + + if (fullResource != null) + { + orgList.Orgs.TryGetValue(fullResource.HasCompetentAuthority.Orgcode.ToLower(), out Org organization); + + accessPackage.Services.Add(new AccessPackageService() + { + Identifier = resourceMatch.Value, + Title = fullResource?.Title, + HasCompetentAuthority = fullResource.HasCompetentAuthority, + LogoUrl = organization.Logo + }); + } + + }); + }); + + return Ok(accessPackages); + } + [HttpGet] [Route("designer/api/{org}/resources/altinn2linkservices/{env}")] public async Task>> GetAltinn2LinkServices(string org, string env) diff --git a/backend/src/Designer/Models/AccessPackage.cs b/backend/src/Designer/Models/AccessPackage.cs new file mode 100644 index 00000000000..7fe0b09aa0e --- /dev/null +++ b/backend/src/Designer/Models/AccessPackage.cs @@ -0,0 +1,23 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Altinn.Studio.Designer.Models +{ + public class AccessPackage + { + public required string Id { get; set; } + + public required string Urn { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public List Tags { get; set; } = []; + + public AccessPackageArea Area { get; set; } + + public List Services { get; set; } = []; + } +} diff --git a/backend/src/Designer/Models/AccessPackageArea.cs b/backend/src/Designer/Models/AccessPackageArea.cs new file mode 100644 index 00000000000..e784f740ef1 --- /dev/null +++ b/backend/src/Designer/Models/AccessPackageArea.cs @@ -0,0 +1,17 @@ +#nullable enable + +namespace Altinn.Studio.Designer.Models +{ + public class AccessPackageArea + { + public required string Id { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public string ShortDescription { get; set; } + + public string IconName { get; set; } + } +} diff --git a/backend/src/Designer/Models/AccessPackageService.cs b/backend/src/Designer/Models/AccessPackageService.cs new file mode 100644 index 00000000000..2def32383f4 --- /dev/null +++ b/backend/src/Designer/Models/AccessPackageService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Altinn.Studio.Designer.Models +{ + public class AccessPackageService + { + public string Identifier { get; set; } + + public Dictionary Title { get; set; } + + public CompetentAuthority HasCompetentAuthority { get; set; } + + public string LogoUrl { get; set; } + } +} diff --git a/backend/src/Designer/Models/AccessPackageTag.cs b/backend/src/Designer/Models/AccessPackageTag.cs new file mode 100644 index 00000000000..806c4270137 --- /dev/null +++ b/backend/src/Designer/Models/AccessPackageTag.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Altinn.Studio.Designer.Models +{ + public class AccessPackageTag + { + public required string Id { get; set; } + + public string Name { get; set; } + } +} diff --git a/backend/src/Designer/Models/AttributeMatchV2.cs b/backend/src/Designer/Models/AttributeMatchV2.cs new file mode 100644 index 00000000000..a6facac1027 --- /dev/null +++ b/backend/src/Designer/Models/AttributeMatchV2.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace Altinn.Studio.Designer.Models +{ + /// + /// This model describes a pair of AttributeId and AttributeValue for use in matching in XACML policies, for instance a resource, a user, a party or an action. + /// + public class AttributeMatchV2 + { + /// + /// Gets or sets the attribute id for the match + /// + [Required] + public required string Type { get; set; } + + /// + /// Gets or sets the attribute value for the match + /// + [Required] + public required string Value { get; set; } + + /// + /// The urn for the attribute + /// + [Required] + public required string Urn { get; set; } + } +} diff --git a/backend/src/Designer/Models/Dto/SubjectResourcesDto.cs b/backend/src/Designer/Models/Dto/SubjectResourcesDto.cs new file mode 100644 index 00000000000..7c5f1fb585f --- /dev/null +++ b/backend/src/Designer/Models/Dto/SubjectResourcesDto.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Altinn.Studio.Designer.Models.Dto +{ + public class SubjectResourcesDto + { + public List Data { get; set; } + } +} diff --git a/backend/src/Designer/Models/SubjectResources.cs b/backend/src/Designer/Models/SubjectResources.cs new file mode 100644 index 00000000000..1c752bd749b --- /dev/null +++ b/backend/src/Designer/Models/SubjectResources.cs @@ -0,0 +1,21 @@ +#nullable enable +using System.Collections.Generic; + +namespace Altinn.Studio.Designer.Models +{ + /// + /// Defines resources that a given subject have access to + /// + public class SubjectResources + { + /// + /// The subject + /// + public required AttributeMatchV2 Subject { get; set; } + + /// + /// List of resources that the given subject has access to + /// + public required List Resources { get; set; } + } +} diff --git a/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs b/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs index b0ec8376379..beb2b946908 100644 --- a/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs +++ b/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs @@ -628,6 +628,25 @@ string env return removeResourceAccessListResponse.StatusCode; } + public async Task> GetSubjectResources(List subjects, string env) + { + string resourceRegisterUrl = GetResourceRegistryBaseUrl(env); + string url = $"{resourceRegisterUrl}/resourceregistry/api/v1/resource/bysubjects"; + + string serializedContent = JsonSerializer.Serialize(subjects, _serializerOptions); + using HttpRequestMessage getSubjectResourcesRequest = new HttpRequestMessage() + { + RequestUri = new Uri(url), + Method = HttpMethod.Post, + Content = new StringContent(serializedContent, Encoding.UTF8, "application/json"), + }; + using HttpResponseMessage response = await _httpClient.SendAsync(getSubjectResourcesRequest); + response.EnsureSuccessStatusCode(); + + SubjectResourcesDto responseContent = await response.Content.ReadAsAsync(); + return responseContent.Data; + } + private async Task> GetBrregParties(string url) { HttpResponseMessage enheterResponse = await _httpClient.GetAsync(url); diff --git a/backend/src/Designer/Services/Interfaces/IResourceRegistry.cs b/backend/src/Designer/Services/Interfaces/IResourceRegistry.cs index ddaa59ea3fb..4b765c05270 100644 --- a/backend/src/Designer/Services/Interfaces/IResourceRegistry.cs +++ b/backend/src/Designer/Services/Interfaces/IResourceRegistry.cs @@ -169,5 +169,7 @@ public interface IResourceRegistry /// Chosen environment /// HTTP status code of the operation. 204 No content if remove was successful Task RemoveResourceAccessList(string org, string resourceId, string listId, string env); + + Task> GetSubjectResources(List subjects, string env); } } diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 3040760c215..13c3f96dfeb 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -545,6 +545,13 @@ "overview.reset_repo_confirm_info": "Du sletter nå alle endringer du har gjort på {{repositoryName}}. Du kan ikke gjenopprette dem.", "overview.reset_repo_confirm_repo_name": "Skriv inn navnet på appen for å bekrefte at du vil slette", "overview.reset_repo_loading": "Sletter...", + "policy_editor.access_package_add": "legg til tilgangspakke {{packageName}}", + "policy_editor.access_package_header": "Tilgangspakker", + "policy_editor.access_package_no_services": "Denne tilgangspakken inneholder ingen tjenester enda", + "policy_editor.access_package_remove": "fjern tilgangspakke {{packageName}}", + "policy_editor.access_package_selected_count": "tilgangspakker valgt i denne kategorien", + "policy_editor.access_package_warning_body": "Altinn-rollene fases snart ut, og da vil rollene som er lagt til, ikke lenger være gyldig. Du må derfor legge til minst en Tilgangspakke for å unngå at regelen blir ugyldig.", + "policy_editor.access_package_warning_header": "Tilgangspakker tar over for Altinn-rollene", "policy_editor.action_complete": "Bekreft mottatt tjenesteeier", "policy_editor.action_confirm": "Bekreft", "policy_editor.action_delete": "Slett", diff --git a/frontend/packages/policy-editor/src/PolicyEditor.tsx b/frontend/packages/policy-editor/src/PolicyEditor.tsx index 3cf74cafe0b..33f994cdc5a 100644 --- a/frontend/packages/policy-editor/src/PolicyEditor.tsx +++ b/frontend/packages/policy-editor/src/PolicyEditor.tsx @@ -8,6 +8,7 @@ import type { PolicySubject, RequiredAuthLevel, PolicyEditorUsage, + PolicyAccessPackage, } from './types'; import { mapPolicyRulesBackendObjectToPolicyRuleCard, @@ -25,6 +26,7 @@ export type PolicyEditorProps = { policy: Policy; actions: PolicyAction[]; subjects: PolicySubject[]; + accessPackages?: PolicyAccessPackage[]; resourceId?: string; onSave: (policy: Policy) => void; // MAYBE MOVE TO CONTEXT showAllErrors: boolean; @@ -35,6 +37,7 @@ export const PolicyEditor = ({ policy, actions, subjects, + accessPackages, resourceId, onSave, showAllErrors, @@ -75,6 +78,7 @@ export const PolicyEditor = ({ setPolicyRules={setPolicyRules} actions={actions} subjects={subjects} + accessPackages={accessPackages ?? []} usageType={usageType} resourceType={resourceType} showAllErrors={showAllErrors} diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/PolicyAccessPackageAccordion.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/PolicyAccessPackageAccordion.module.css new file mode 100644 index 00000000000..61a8ee11331 --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/PolicyAccessPackageAccordion.module.css @@ -0,0 +1,35 @@ +:root { + --logo-size: 30px; +} + +.accessPackageAccordion { + background-color: #e9f5ff; +} + +.serviceContainer { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--fds-spacing-2); + background-color: #fff; + padding: var(--fds-spacing-2); + margin-top: var(--fds-spacing-2); + min-height: var(--fds-spacing-8); + box-shadow: + 0px 1px 2px 0px #0000001f, + 0px 0px 1px 0px #00000029; +} + +.serviceLabel { + flex: 1; +} + +.logo { + max-height: var(--logo-size); + max-width: var(--logo-size); +} + +.emptyLogo { + height: var(--logo-size); + width: var(--logo-size); +} diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/PolicyAccessPackageAccordion.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/PolicyAccessPackageAccordion.test.tsx new file mode 100644 index 00000000000..99e84f44cbd --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/PolicyAccessPackageAccordion.test.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { PolicyAccessPackageAccordion } from './PolicyAccessPackageAccordion'; + +const defaultAccessPackageProp = { + id: 'urn:altinn:accesspackage:sjofart', + urn: 'urn:altinn:accesspackage:sjofart', + name: 'Sjøfart', + description: + 'Denne fullmakten gir tilgang til alle tjenester knyttet til skipsarbeidstakere og fartøy til sjøs. Ved regelverksendringer eller innføring av nye digitale tjenester kan det bli endringer i tilganger som fullmakten gir.', + services: [], + area: { + id: 'transport-id', + name: 'Transport og lagring', + description: '', + iconName: '', + shortDescription: '', + }, +}; + +const resource = { + identifier: 'kravogbetaling', + title: { + nb: 'Krav og betaling', + nn: 'Krav og betaling', + en: 'Krav og betaling', + }, + hasCompetentAuthority: { + name: { + nb: 'Skatteetaten', + nn: 'Skatteetaten', + en: 'Skatteetaten', + }, + organization: '974761076', + orgcode: 'skd', + }, + logoUrl: '', +}; + +describe('PolicyAccessPackageAccordion', () => { + afterEach(jest.clearAllMocks); + + it('should show text if access package contains no services', async () => { + const user = userEvent.setup(); + render( + } + />, + ); + + const accordionButton = screen.getByRole('button'); + await user.click(accordionButton); + + expect( + screen.getByText(textMock('policy_editor.access_package_no_services')), + ).toBeInTheDocument(); + }); + + it('should show list of services', async () => { + const user = userEvent.setup(); + + render( + } + />, + ); + + const accordionButton = screen.getByRole('button'); + await user.click(accordionButton); + + expect(screen.getByText(resource.title.nb)).toBeInTheDocument(); + }); + + it('should show logo for services', async () => { + const user = userEvent.setup(); + + render( + } + />, + ); + + const accordionButton = screen.getByRole('button'); + await user.click(accordionButton); + + expect(screen.getByAltText(resource.hasCompetentAuthority.name.nb)).toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/PolicyAccessPackageAccordion.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/PolicyAccessPackageAccordion.tsx new file mode 100644 index 00000000000..931da80a404 --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/PolicyAccessPackageAccordion.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import classes from './PolicyAccessPackageAccordion.module.css'; +import type { PolicyAccessPackage } from '@altinn/policy-editor/types'; +import { Paragraph } from '@digdir/designsystemet-react'; +import { StudioLabelAsParagraph } from '@studio/components'; +import { PolicyAccordion } from '../PolicyAccordion/PolicyAccordion'; + +interface PolicyAccessPackageAccordionProps { + accessPackage: PolicyAccessPackage; + selectedLanguage: 'nb' | 'nn' | 'en'; + selectPackageElement: React.ReactNode; +} + +export const PolicyAccessPackageAccordion = ({ + accessPackage, + selectedLanguage, + selectPackageElement, +}: PolicyAccessPackageAccordionProps): React.ReactElement => { + const { t } = useTranslation(); + + return ( +
+ + {accessPackage.services.length > 0 ? ( + accessPackage.services.map((resource) => { + return ( +
+ {resource.logoUrl ? ( + {resource.hasCompetentAuthority.name[selectedLanguage]} + ) : ( +
+ )} + + {resource.title[selectedLanguage]} + + + {resource.hasCompetentAuthority.name[selectedLanguage]} + +
+ ); + }) + ) : ( + {t('policy_editor.access_package_no_services')} + )} + +
+ ); +}; diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/index.ts new file mode 100644 index 00000000000..2b6e8787767 --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackageAccordion/index.ts @@ -0,0 +1 @@ +export { PolicyAccessPackageAccordion } from './PolicyAccessPackageAccordion'; diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackages.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackages.module.css new file mode 100644 index 00000000000..6866eb3d386 --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackages.module.css @@ -0,0 +1,17 @@ +.accessPackages { + display: flex; + flex-direction: column; + gap: var(--fds-spacing-2); + margin-top: var(--fds-spacing-8); +} + +.accordionContent { + display: flex; + flex-direction: column; + gap: var(--fds-spacing-2); +} + +.accordionCheckbox { + display: block; + padding: 0 var(--fds-spacing-4); +} diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackages.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackages.test.tsx new file mode 100644 index 00000000000..220246a3f34 --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackages.test.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { PolicyEditorContext } from '@altinn/policy-editor/contexts/PolicyEditorContext'; +import { PolicyAccessPackages } from './PolicyAccessPackages'; +import { PolicyRuleContext } from '@altinn/policy-editor/contexts/PolicyRuleContext'; +import { textMock } from '@studio/testing/mocks/i18nMock'; +import { mockPolicyRuleContextValue } from '../../../../../test/mocks/policyRuleContextMock'; +import { mockPolicyEditorContextValue } from '../../../../../test/mocks/policyEditorContextMock'; + +const accessPackageAreaTransport = { + id: 'transport-area', + name: 'Lagring og transport', + description: '', + iconName: 'TruckIcon', + shortDescription: '', +}; + +const sjofartPackage = { + id: 'urn:altinn:accesspackage:sjofart', + urn: 'urn:altinn:accesspackage:sjofart', + name: 'Sjøfart', + description: '', + services: [], + area: accessPackageAreaTransport, +}; + +const lufttransportPackage = { + id: 'urn:altinn:accesspackage:lufttransport', + urn: 'urn:altinn:accesspackage:lufttransport', + name: 'Lufttransport', + description: '', + services: [], + area: accessPackageAreaTransport, +}; + +const accessPackages = [sjofartPackage, lufttransportPackage]; + +describe('PolicyAccessPackages', () => { + afterEach(jest.clearAllMocks); + + it('should call add service when access package is checked', async () => { + const user = userEvent.setup(); + renderAccessPackages(); + + const accordionButton = screen.getByRole('button'); + await user.click(accordionButton); + + const packageCheckbox = screen.getByLabelText( + textMock('policy_editor.access_package_add', { + packageName: sjofartPackage.name, + }), + ); + + await user.click(packageCheckbox); + + expect(packageCheckbox).toBeChecked(); + }); + + it('should call remove service when access package is unchecked', async () => { + const user = userEvent.setup(); + renderAccessPackages(); + + const accordionButton = screen.getByRole('button'); + await user.click(accordionButton); + + const packageCheckbox = screen.getByLabelText( + textMock('policy_editor.access_package_remove', { + packageName: lufttransportPackage.name, + }), + ); + + await user.click(packageCheckbox); + + expect(packageCheckbox).not.toBeChecked(); + }); +}); + +const renderAccessPackages = () => { + return render( + + + + + , + ); +}; diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackages.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackages.tsx new file mode 100644 index 00000000000..dc741e085c0 --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccessPackages.tsx @@ -0,0 +1,126 @@ +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Paragraph, Alert, CheckboxGroup, Checkbox } from '@digdir/designsystemet-react'; +import { StudioLabelAsParagraph } from '@studio/components'; +import type { PolicyAccessPackage } from '../../../../types'; +import { getUpdatedRules } from '../../../../utils/PolicyRuleUtils'; +import { usePolicyEditorContext } from '../../../../contexts/PolicyEditorContext'; +import { usePolicyRuleContext } from '../../../../contexts/PolicyRuleContext'; +import classes from './PolicyAccessPackages.module.css'; +import { PolicyAccessPackageAccordion } from './PolicyAccessPackageAccordion'; +import { PolicyAccordion } from './PolicyAccordion/PolicyAccordion'; +import { groupAccessPackagesByArea } from '@altinn/policy-editor/utils'; + +const CHECKED_VALUE = 'on'; +const selectedLanguage = 'nb'; + +export const PolicyAccessPackages = (): React.ReactElement => { + const { t } = useTranslation(); + const { policyRules, accessPackages, setPolicyRules, savePolicy } = usePolicyEditorContext(); + const { policyRule } = usePolicyRuleContext(); + + const [chosenAccessPackages, setChosenAccessPackages] = useState( + policyRule.accessPackages, + ); + + const groupedAccessPackagesByArea = useMemo(() => { + return groupAccessPackagesByArea(accessPackages); + }, [accessPackages]); + + const onPackageSelectChange = (accessPackage: PolicyAccessPackage): void => { + const isSelected = chosenAccessPackages.includes(accessPackage.urn); + if (isSelected) { + handleRemoveAccessPackage(accessPackage.urn); + } else { + handleAddAccessPackage(accessPackage.urn); + } + }; + + const handleRemoveAccessPackage = (packageUrn: string): void => { + setChosenAccessPackages((oldUrns) => oldUrns.filter((urn) => urn !== packageUrn)); + const urnsToSave = policyRule.accessPackages.filter((x) => x !== packageUrn); + + handleAccessPackageChange(urnsToSave); + }; + + const handleAddAccessPackage = (packageUrn: string): void => { + setChosenAccessPackages((oldUrns) => [...oldUrns, packageUrn]); + const urnsToSave = [...policyRule.accessPackages, packageUrn]; + + handleAccessPackageChange(urnsToSave); + }; + + const handleAccessPackageChange = (newSelectedAccessPackageUrns: string[]): void => { + const updatedRules = getUpdatedRules( + { + ...policyRule, + accessPackages: newSelectedAccessPackageUrns, + }, + policyRule.ruleId, + policyRules, + ); + + setPolicyRules(updatedRules); + savePolicy(updatedRules); + }; + + return ( +
+ + + {t('policy_editor.access_package_warning_header')} + + {t('policy_editor.access_package_warning_body')} + + + {t('policy_editor.access_package_header')} + + {groupedAccessPackagesByArea.map(({ area, packages }) => { + const numberChosenInArea = packages.filter((pack) => + chosenAccessPackages.includes(pack.urn), + ).length; + + return ( + + {area.description} + {packages.map((accessPackage) => { + const isChecked = chosenAccessPackages.includes(accessPackage.urn); + const checkboxLabel = t( + isChecked + ? 'policy_editor.access_package_remove' + : 'policy_editor.access_package_add', + { + packageName: accessPackage.name, + }, + ); + const packageCheckbox = ( + onPackageSelectChange(accessPackage)} + > + + + ); + return ( + + ); + })} + + ); + })} +
+ ); +}; diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/PolicyAccordion.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/PolicyAccordion.module.css new file mode 100644 index 00000000000..cdf249757c1 --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/PolicyAccordion.module.css @@ -0,0 +1,62 @@ +.accordion { + margin-top: var(--fds-spacing-2); + box-shadow: + 0px 1px 2px 0px #0000001f, + 0px 0px 1px 0px #00000029; +} + +.accordionHeader { + display: flex; + align-items: center; + --iconColor: #d3eafd; +} + +.selectedAccordionHeader { + background-color: #d3eafd; + --iconColor: #fff; +} + +.accordionButton { + width: 100%; + display: flex; + gap: var(--fds-spacing-2); + align-items: center; +} + +.accordionIcon { + font-size: 1.75rem; + color: #111d46; +} + +.accordionContent { + padding: var(--fds-spacing-4); +} + +.accordionTitle { + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; + flex: 1; +} + +.accordionSubTitle { + color: #5d6470; + font-size: 12px; +} + +.iconContainer { + background-color: var(--iconColor); + border-radius: 3px; + padding: 6px; +} + +.visuallyHidden { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/PolicyAccordion.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/PolicyAccordion.test.tsx new file mode 100644 index 00000000000..234ee497fc8 --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/PolicyAccordion.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { PolicyAccordion } from './PolicyAccordion'; + +describe('PolicyAccordion', () => { + afterEach(jest.clearAllMocks); + + it('should render selected count', () => { + render( + +
+ , + ); + + expect(screen.getByText(333)).toBeInTheDocument(); + }); + + it('should show children when expanded', async () => { + const user = userEvent.setup(); + const childElementText = 'TEST CHILD ELEMENT'; + render( + +
{childElementText}
+
, + ); + + const accordionButton = screen.getByRole('button'); + await user.click(accordionButton); + + expect(screen.getByText(childElementText)).toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/PolicyAccordion.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/PolicyAccordion.tsx new file mode 100644 index 00000000000..47a4657480e --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/PolicyAccordion.tsx @@ -0,0 +1,78 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import cn from 'classnames'; +import { Tag } from '@digdir/designsystemet-react'; +import { StudioButton, StudioLabelAsParagraph } from '@studio/components'; +import * as StudioIcons from '@studio/icons'; +import { ChevronDownIcon, ChevronUpIcon } from '@studio/icons'; +import classes from './PolicyAccordion.module.css'; + +interface PolicyAccordion { + icon?: string; + title: string; + subTitle: string; + selectedCount?: number; + extraHeaderContent?: React.ReactNode; + children: React.ReactNode; +} + +export const PolicyAccordion = ({ + icon, + title, + subTitle, + selectedCount, + extraHeaderContent, + children, +}: PolicyAccordion): React.ReactNode => { + const { t } = useTranslation(); + + const [isExpanded, setIsExpanded] = useState(false); + const IconComponent = StudioIcons[icon]; + + return ( +
+
0 ? classes.selectedAccordionHeader : '', + )} + > + setIsExpanded((oldIsExpanded) => !oldIsExpanded)} + > +
+ {icon && Object.keys(StudioIcons).includes(icon) && ( + + )} +
+ {title} +
{subTitle}
+
+ {selectedCount > 0 && ( + + {selectedCount} +
+ {' '} + {t('policy_editor.access_package_selected_count')} +
+
+ )} +
+ {isExpanded ? ( + + ) : ( + + )} +
+ {extraHeaderContent} +
+ {isExpanded &&
{children}
} +
+ ); +}; diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/index.ts new file mode 100644 index 00000000000..915d9c68a38 --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/PolicyAccordion/index.ts @@ -0,0 +1 @@ +export { PolicyAccordion } from './PolicyAccordion'; diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/index.ts new file mode 100644 index 00000000000..a038187fa64 --- /dev/null +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyAccessPackages/index.ts @@ -0,0 +1 @@ +export { PolicyAccessPackages } from './PolicyAccessPackages'; diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRule.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRule.tsx index 11c34063201..724f8c9d28f 100644 --- a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRule.tsx +++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRule.tsx @@ -12,6 +12,8 @@ import { PolicyRuleErrorMessage } from './PolicyRuleErrorMessage'; import { getNewRuleId } from '../../../utils'; import { usePolicyEditorContext } from '../../../contexts/PolicyEditorContext'; import { ObjectUtils } from '@studio/pure-functions'; +import { PolicyAccessPackages } from './PolicyAccessPackages'; +import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; export type PolicyRuleProps = { policyRule: PolicyRuleCard; @@ -83,6 +85,7 @@ export const PolicyRule = ({ + {shouldDisplayFeature('accessPackages') && } {showErrors && } diff --git a/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/PolicyEditorContext.tsx b/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/PolicyEditorContext.tsx index a93948441c1..59eca9b6380 100644 --- a/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/PolicyEditorContext.tsx +++ b/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/PolicyEditorContext.tsx @@ -1,11 +1,18 @@ import React, { createContext, useContext } from 'react'; -import type { PolicyAction, PolicyEditorUsage, PolicyRuleCard, PolicySubject } from '../../types'; +import type { + PolicyAccessPackage, + PolicyAction, + PolicyEditorUsage, + PolicyRuleCard, + PolicySubject, +} from '../../types'; export type PolicyEditorContextProps = { policyRules: PolicyRuleCard[]; setPolicyRules: React.Dispatch>; actions: PolicyAction[]; subjects: PolicySubject[]; + accessPackages: PolicyAccessPackage[]; usageType: PolicyEditorUsage; resourceType: string; resourceId: string; diff --git a/frontend/packages/policy-editor/src/index.ts b/frontend/packages/policy-editor/src/index.ts index a1142e85eb9..69fb3ed9e06 100644 --- a/frontend/packages/policy-editor/src/index.ts +++ b/frontend/packages/policy-editor/src/index.ts @@ -3,6 +3,8 @@ export type { Policy, PolicyAction, PolicySubject, + PolicyAccessPackageArea, + PolicyAccessPackage, PolicyRule, PolicyRuleResource, RequiredAuthLevel, diff --git a/frontend/packages/policy-editor/src/types/index.ts b/frontend/packages/policy-editor/src/types/index.ts index aaede8e41ac..68ca4a173db 100644 --- a/frontend/packages/policy-editor/src/types/index.ts +++ b/frontend/packages/policy-editor/src/types/index.ts @@ -2,6 +2,7 @@ export interface PolicyRuleCard { ruleId: string; description: string; subject: string[]; + accessPackages?: string[]; actions: string[]; resources: PolicyRuleResource[][]; } @@ -18,6 +19,42 @@ export interface PolicySubject { subjectDescription: string; } +export interface PolicyAccessPackage { + id: string; + urn: string; + name: string; + description: string; + services: AccessPackageResource[]; + area: PolicyAccessPackageArea; +} + +export interface PolicyAccessPackageArea { + id: string; + name: string; + description: string; + iconName: string; + shortDescription: string; +} + +export interface AccessPackageResource { + identifier: string; + title: { + nb: string; + nn: string; + en: string; + }; + hasCompetentAuthority?: { + name: { + nb: string; + nn: string; + en: string; + }; + organization: string; + orgcode: string; + }; + logoUrl: string; +} + export interface PolicyAction { actionId: string; actionTitle: string; @@ -29,6 +66,7 @@ export interface PolicyRule { description: string; subject: string[]; actions: string[]; + accessPackages?: string[]; resources: string[][]; } diff --git a/frontend/packages/policy-editor/src/utils/index.ts b/frontend/packages/policy-editor/src/utils/index.ts index cc74d1513ca..d6d4eddb8e3 100644 --- a/frontend/packages/policy-editor/src/utils/index.ts +++ b/frontend/packages/policy-editor/src/utils/index.ts @@ -1,5 +1,7 @@ import { ObjectUtils } from '@studio/pure-functions'; import type { + PolicyAccessPackage, + PolicyAccessPackageArea, PolicyAction, PolicyEditorUsage, PolicyRule, @@ -16,6 +18,7 @@ export const emptyPolicyRule: PolicyRuleCard = { resources: [], actions: [], subject: [], + accessPackages: [], description: '', }; @@ -63,6 +66,7 @@ export const mapPolicyRulesBackendObjectToPolicyRuleCard = ( actions: r.actions, description: r.description, subject: subjectIds, + accessPackages: r.accessPackages, resources: mappedResources, }; }); @@ -114,6 +118,7 @@ export const mapPolicyRuleToPolicyRuleBackendObject = ( description: policyRule.description, subject: subject, actions: policyRule.actions, + accessPackages: policyRule.accessPackages, resources: resources, }; }; @@ -206,6 +211,32 @@ export const mergeSubjectsFromPolicyWithSubjectOptions = ( return copiedSubjects; }; +export const groupAccessPackagesByArea = (accessPackages: PolicyAccessPackage[]) => { + // create temp dictionary. Use area.id as key + const groupedByAreaDict: { + [areaId: string]: { + area: PolicyAccessPackageArea; + packages: PolicyAccessPackage[]; + }; + } = {}; + + // add each package to corresponding area.id + accessPackages.forEach((accessPackage) => { + if (!groupedByAreaDict[accessPackage.area.id]) { + groupedByAreaDict[accessPackage.area.id] = { + area: accessPackage.area, + packages: [], + }; + } + groupedByAreaDict[accessPackage.area.id].packages.push(accessPackage); + }); + + // map dictionary to an array + return Object.keys(groupedByAreaDict).map((key) => { + return { ...groupedByAreaDict[key] }; + }); +}; + export const convertSubjectStringToSubjectId = (subjectString: string): string => { const lastColonIndex = subjectString.lastIndexOf(':'); // The final element is the id diff --git a/frontend/packages/policy-editor/test/mocks/policyEditorContextMock.ts b/frontend/packages/policy-editor/test/mocks/policyEditorContextMock.ts index 986a74e0ee8..67980a9d7b6 100644 --- a/frontend/packages/policy-editor/test/mocks/policyEditorContextMock.ts +++ b/frontend/packages/policy-editor/test/mocks/policyEditorContextMock.ts @@ -16,6 +16,7 @@ export const mockPolicyEditorContextValue: PolicyEditorContextProps = { setPolicyRules: jest.fn(), actions: mockActions, subjects: mockSubjects, + accessPackages: [], usageType: mockUsageType, resourceType: mockResourceType, showAllErrors: false, @@ -28,6 +29,7 @@ export const mockPolicyEditorContextValueWithSingleNarrowingPolicy: PolicyEditor setPolicyRules: jest.fn(), actions: mockActions, subjects: mockSubjects, + accessPackages: [], usageType: mockUsageType, resourceType: mockResourceType, showAllErrors: false, diff --git a/frontend/packages/policy-editor/test/mocks/policyRuleMocks.ts b/frontend/packages/policy-editor/test/mocks/policyRuleMocks.ts index 8cc632c4d2d..d7acb9c447a 100644 --- a/frontend/packages/policy-editor/test/mocks/policyRuleMocks.ts +++ b/frontend/packages/policy-editor/test/mocks/policyRuleMocks.ts @@ -21,6 +21,7 @@ export const mockPolicyRuleCard1: PolicyRuleCard = { description: '', subject: [mockSubjectId1, mockSubjectId3], actions: [mockAction1.actionId, mockAction2.actionId, mockAction4.actionId], + accessPackages: [], resources: mockPolicyRuleResources, }; export const mockPolicyRuleCard2: PolicyRuleCard = { @@ -28,6 +29,7 @@ export const mockPolicyRuleCard2: PolicyRuleCard = { description: '', subject: [], actions: [], + accessPackages: [], resources: [[]], }; export const mockPolicyRuleCardWithSingleNarrowingPolicy: PolicyRuleCard = { @@ -35,6 +37,7 @@ export const mockPolicyRuleCardWithSingleNarrowingPolicy: PolicyRuleCard = { description: '', subject: [mockSubjectId1, mockSubjectId3], actions: [mockAction1.actionId, mockAction2.actionId, mockAction4.actionId], + accessPackages: [], resources: mockPolicyRuleResourcesWithSingleNarrowingPolicy, }; export const mockPolicyRuleCards: PolicyRuleCard[] = [mockPolicyRuleCard1, mockPolicyRuleCard2]; @@ -44,6 +47,7 @@ export const mockPolicyRule1: PolicyRule = { description: '', subject: [mockSubjectBackendString1, mockSubjectBackendString3], actions: [mockAction1.actionId, mockAction2.actionId, mockAction4.actionId], + accessPackages: [], resources: mockPolicyResources, }; export const mockPolicyRule2: PolicyRule = { @@ -52,5 +56,6 @@ export const mockPolicyRule2: PolicyRule = { subject: [], actions: [], resources: [[]], + accessPackages: [], }; export const mockPolicyRules: PolicyRule[] = [mockPolicyRule1, mockPolicyRule2]; diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index fb6e9c76aa9..3d5c588f09d 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -129,6 +129,7 @@ export const appPolicyPath = (org, app) => `${basePath}/${org}/${app}/policy`; / export const resourcePolicyPath = (org, repo, id) => `${basePath}/${org}/${repo}/policy/${id}`; // Get, Put export const resourceActionsPath = (org, repo) => `${basePath}/${org}/${repo}/policy/actionoptions`; // Get export const resourceSubjectsPath = (org, repo) => `${basePath}/${org}/${repo}/policy/subjectoptions`; // Get +export const resourceAccessPackagesPath = (org) => `${basePath}/${org}/resources/accesspackages`; // Get export const resourcePublishStatusPath = (org, repo, id) => `${basePath}/${org}/resources/publishstatus/${repo}/${id}`; // Get export const resourceListPath = (org) => `${basePath}/${org}/resources/resourcelist?includeEnvResources=true`; // Get export const resourceCreatePath = (org) => `${basePath}/${org}/resources/addresource`; // Post diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index 0e5f047156b..166e969defe 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -53,6 +53,7 @@ import { repoDiffPath, getImageFileNamesPath, validateImageFromExternalUrlPath, + resourceAccessPackagesPath, } from './paths'; import type { AppReleasesResponse, DataModelMetadataResponse, SearchRepoFilterParams, SearchRepositoryResponse } from 'app-shared/types/api'; import type { DeploymentsResponse } from 'app-shared/types/api/DeploymentsResponse'; @@ -72,7 +73,7 @@ import type { WidgetSettingsResponse } from 'app-shared/types/widgetTypes'; import { buildQueryParams } from 'app-shared/utils/urlUtils'; import { orgListUrl } from '../cdn-paths'; import type { JsonSchema } from 'app-shared/types/JsonSchema'; -import type { PolicyAction, PolicySubject } from '@altinn/policy-editor'; +import type { PolicyAction, PolicySubject, PolicyAccessPackage } from '@altinn/policy-editor'; import type { BrregPartySearchResult, BrregSubPartySearchResult, AccessList, Resource, ResourceListItem, ResourceVersionStatus, Validation, AccessListsResponse, AccessListMembersResponse, DelegationCountOverview } from 'app-shared/types/ResourceAdm'; import type { AppConfig } from 'app-shared/types/AppConfig'; import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata'; @@ -130,6 +131,7 @@ export const getAltinn2LinkServices = (org: string, environment: string) => get< export const getPolicyActions = (org: string, repo: string) => get(resourceActionsPath(org, repo)); export const getPolicy = (org: string, repo: string, id: string) => get(resourcePolicyPath(org, repo, id)); export const getPolicySubjects = (org: string, repo: string) => get(resourceSubjectsPath(org, repo)); +export const getAccessPackages = (org: string) => get(resourceAccessPackagesPath(org)); export const getResource = (org: string, repo: string, id: string) => get(resourceSinglePath(org, repo, id)); export const getResourceList = (org: string) => get(resourceListPath(org)); export const getResourcePublishStatus = (org: string, repo: string, id: string) => get(resourcePublishStatusPath(org, repo, id)); diff --git a/frontend/packages/shared/src/hooks/queries/index.ts b/frontend/packages/shared/src/hooks/queries/index.ts index 32e273ad0d3..71edd3d5790 100644 --- a/frontend/packages/shared/src/hooks/queries/index.ts +++ b/frontend/packages/shared/src/hooks/queries/index.ts @@ -10,3 +10,4 @@ export { useTextResourcesQuery } from './useTextResourcesQuery'; export { useUserQuery } from './useUserQuery'; export { useResourcePolicyActionsQuery } from './useResourcePolicyActionsQuery'; export { useResourcePolicySubjectsQuery } from './useResourcePolicySubjectsQuery'; +export { useResourceAccessPackagesQuery } from './useResourceAccessPackagesQuery'; diff --git a/frontend/packages/shared/src/hooks/queries/useResourceAccessPackagesQuery.ts b/frontend/packages/shared/src/hooks/queries/useResourceAccessPackagesQuery.ts new file mode 100644 index 00000000000..3e0d5792afd --- /dev/null +++ b/frontend/packages/shared/src/hooks/queries/useResourceAccessPackagesQuery.ts @@ -0,0 +1,24 @@ +import type { UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; +import type { PolicyAccessPackage } from '@altinn/policy-editor'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import type { AxiosError } from 'axios'; + +/** + * Query to get the list of access package categories + * + * @param org the organisation of the user + * + * @returns UseQueryResult with a list of access package categories + */ +export const useResourceAccessPackagesQuery = ( + org: string, +): UseQueryResult => { + const { getAccessPackages } = useServicesContext(); + + return useQuery({ + queryKey: [QueryKey.ResourcePolicyAccessPackages, org], + queryFn: () => getAccessPackages(org), + }); +}; diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts index 03f83e9c251..da25ce617f8 100644 --- a/frontend/packages/shared/src/mocks/queriesMock.ts +++ b/frontend/packages/shared/src/mocks/queriesMock.ts @@ -142,6 +142,7 @@ export const queriesMock: ServicesContextProps = { getPolicyActions: jest.fn().mockImplementation(() => Promise.resolve([])), getPolicy: jest.fn().mockImplementation(() => Promise.resolve(policy)), getPolicySubjects: jest.fn().mockImplementation(() => Promise.resolve([])), + getAccessPackages: jest.fn().mockImplementation(() => Promise.resolve([])), getResource: jest.fn().mockImplementation(() => Promise.resolve(resource)), getResourceList: jest.fn().mockImplementation(() => Promise.resolve([])), getResourcePublishStatus: jest diff --git a/frontend/packages/shared/src/types/QueryKey.ts b/frontend/packages/shared/src/types/QueryKey.ts index f3542c6f927..0de2d6d63f9 100644 --- a/frontend/packages/shared/src/types/QueryKey.ts +++ b/frontend/packages/shared/src/types/QueryKey.ts @@ -50,6 +50,7 @@ export enum QueryKey { ResourcePolicy = 'ResourcePolicy', ResourcePolicyActions = 'ResourcePolicyActions', ResourcePolicySubjects = 'ResourcePolicySubjects', + ResourcePolicyAccessPackages = 'ResourcePolicyAccessPackages', ResourcePublishStatus = 'ResourcePublishStatus', ResourceSectors = 'ResourceSectors', ResourceThematicEurovoc = 'ResourceThematicEurovoc', diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index a610516af17..a60b9e7fe28 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -8,6 +8,7 @@ export type SupportedFeatureFlags = | 'componentConfigBeta' | 'shouldOverrideAppLibCheck' | 'resourceMigration' + | 'accessPackages' | 'multipleDataModelsPerTask' | 'exportForm' | 'addComponentModal' diff --git a/frontend/resourceadm/pages/PolicyEditorPage/PolicyEditorPage.tsx b/frontend/resourceadm/pages/PolicyEditorPage/PolicyEditorPage.tsx index d187a5b86ff..ec137fd762a 100644 --- a/frontend/resourceadm/pages/PolicyEditorPage/PolicyEditorPage.tsx +++ b/frontend/resourceadm/pages/PolicyEditorPage/PolicyEditorPage.tsx @@ -13,6 +13,7 @@ import { useTranslation } from 'react-i18next'; import { useResourcePolicyActionsQuery, useResourcePolicySubjectsQuery, + useResourceAccessPackagesQuery, } from 'app-shared/hooks/queries'; import { useUrlParams } from '../../hooks/useUrlParams'; @@ -49,6 +50,8 @@ export const PolicyEditorPage = ({ org, app, ); + const { data: accessPackages, isPending: isLoadingAccessPackages } = + useResourceAccessPackagesQuery(org); // Mutation function to update policy const { mutate: updatePolicyMutation } = useEditResourcePolicyMutation(org, app, resourceId); @@ -68,7 +71,7 @@ export const PolicyEditorPage = ({ * Displays the content based on the state of the page */ const displayContent = () => { - if (isPolicyPending || isActionPending || isSubjectsPending) { + if (isPolicyPending || isActionPending || isSubjectsPending || isLoadingAccessPackages) { return (