diff --git a/.gitignore b/.gitignore
index b8265dfc..43f397b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,4 @@ provisioning.local.json
IgnoreMeTest.java
dependency.tree
NOTES.txt
+private_key_pkcs8.pem
diff --git a/client/src/__tests__/utils/Utils.test.js b/client/src/__tests__/utils/Utils.test.js
index 2bd413ca..791cfbca 100644
--- a/client/src/__tests__/utils/Utils.test.js
+++ b/client/src/__tests__/utils/Utils.test.js
@@ -1,8 +1,18 @@
import React from "react";
import {
- sanitizeURL
+ sanitizeURL,distinctValues
} from "../../utils/Utils";
test("Test sanitizeURL", () => {
expect(sanitizeURL("https://invite.test2.surfconext.nl")).toEqual("https://invite.test2.surfconext.nl");
});
+
+test("Test distinctValues", () => {
+ const res = distinctValues([{id:"1", val: "val1"}, {id:"1", val: "valX"}, {id:"2", val: "val2"}, {id:"3", val: "val3"}],
+ "id");
+ expect(res.length).toEqual(3);
+
+ const resId = distinctValues([{id:1, val: "val1"}, {id:1, val: "valX"}, {id:2, val: "val2"}, {id:3, val: "val3"}],
+ "id");
+ expect(res.length).toEqual(3);
+});
\ No newline at end of file
diff --git a/client/src/locale/en.js b/client/src/locale/en.js
index 4cb70a53..56f5fce5 100644
--- a/client/src/locale/en.js
+++ b/client/src/locale/en.js
@@ -323,7 +323,7 @@ const en = {
userRoleIcon: "User role accepted by {{name}} at {{createdAt}}",
invitationIcon: "Invitation for {{email}} sent at {{createdAt}} with expiration date {{expiryDate}}",
roleShortName: "The unique short name of the role within a provisioning. It is used to format the urn and therefore not all characters are allowed.",
- roleUrn: "The urn of the role. It is based on the sanitized name and the application identifier. It is used as the unique global identifier of this role and therefore not all characters are allowed.",
+ roleUrn: "The urn of the role. It is based on the sanitized name and the role identifier. It is used as the unique global identifier of this role and therefore not all characters are allowed.",
manageService: "The required service from SURFconext with may have an optional provisioning",
defaultExpiryDays: "The default number of days the role will expiry when a use accepts a invitation for this role",
enforceEmailEqualityTooltip: "When checked the invitee must accept the invitation with the email where the invitation was sent to",
diff --git a/client/src/locale/nl.js b/client/src/locale/nl.js
index 14971149..a4d2d67e 100644
--- a/client/src/locale/nl.js
+++ b/client/src/locale/nl.js
@@ -323,7 +323,7 @@ const nl = {
userRoleIcon: "Gebruikersrol geaccepteerd door {{name}} op {{createdAt}}",
invitationIcon: "Uitnodiging aan {{email}} verstuurd op {{createdAt}} met verloopdatum {{expiryDate}}",
roleShortName: "Een unieke korte naam voor de rol binnen een provisioning. Gebruikt in de urn, dus daarom zijn niet alle tekens toegestaan.",
- roleUrn: "De urn van deze rol. Deze is gebaseerd op de opgeschoonde naam en de applicatie-identifier. Hij wordt gebruikt als de unieke globale identifier van deze rol en daarom zijn niet alle tekens toegestaan.",
+ roleUrn: "De urn van deze rol. Deze is gebaseerd op de opgeschoonde naam en de rol-identifier. Hij wordt gebruikt als de unieke globale identifier van deze rol en daarom zijn niet alle tekens toegestaan.",
manageService: "De vereiste dienst uit SURFconext die optioneel een provisioning heeft.",
defaultExpiryDays: "Het default aantal dagen waarna de rol verloopt, gerekend vanaf het moment dat de gebruiker de uitnodiging voor de rol accepteert.",
enforceEmailEqualityTooltip: "Indien ingeschakeld moet de genodigde de uitnodiging accepteren met een account dat hetzelfde e-mailadres voert als waarheen deze uitnodiging gestuurd is",
diff --git a/client/src/pages/Role.js b/client/src/pages/Role.js
index 7481f171..6a3e1834 100644
--- a/client/src/pages/Role.js
+++ b/client/src/pages/Role.js
@@ -163,7 +163,7 @@ export const Role = () => {
- {`${role.applicationName}`}
+ {`${role.applicationNames}`}
{role.applicationOrganizationName && {` (${role.applicationOrganizationName})`}}
diff --git a/client/src/pages/RoleForm.js b/client/src/pages/RoleForm.js
index 225fd384..8bc312f7 100644
--- a/client/src/pages/RoleForm.js
+++ b/client/src/pages/RoleForm.js
@@ -10,7 +10,7 @@ import {UnitHeader} from "../components/UnitHeader";
import {ReactComponent as RoleIcon} from "@surfnet/sds/icons/illustrative-icons/hierarchy.svg";
import InputField from "../components/InputField";
import {constructShortName} from "../validations/regExps";
-import {isEmpty} from "../utils/Utils";
+import {distinctValues, isEmpty} from "../utils/Utils";
import ErrorIndicator from "../components/ErrorIndicator";
import SelectField from "../components/SelectField";
import {providersToOptions, singleProviderToOption} from "../utils/Manage";
@@ -26,7 +26,7 @@ export const RoleForm = () => {
const required = ["name", "description", "applications"];
const {user, setFlash, config} = useAppStore(state => state);
- const [role, setRole] = useState({name: "", shortName: "", defaultExpiryDays: 0});
+ const [role, setRole] = useState({name: "", shortName: "", defaultExpiryDays: 0, identifier: crypto.randomUUID()});
const [providers, setProviders] = useState([]);
const [isNewRole, setNewRole] = useState(true);
const [loading, setLoading] = useState(true);
@@ -57,9 +57,11 @@ export const RoleForm = () => {
if (user.superUser) {
setProviders(res[newRole ? 0 : 1]);
} else if (user.institutionAdmin) {
- setProviders(user.applications.concat(user.userRoles.map(userRole => userRole.role.application)));
+ setProviders(distinctValues(user.applications
+ .concat(user.userRoles.map(userRole => userRole.role.applicationMaps)
+ .flat()), "id"));
} else {
- setProviders(user.userRoles.map(userRole => userRole.role.application));
+ setProviders(distinctValues(user.userRoles.map(userRole => userRole.role.applicationMaps).flat(), "id"));
}
setNewRole(newRole);
const name = newRole ? "" : res[0].name;
@@ -69,7 +71,7 @@ export const RoleForm = () => {
];
if (newRole) {
const providerOption = singleProviderToOption(user.superUser ? res[0][0] :
- user.institutionAdmin ? user.applications[0] : user.userRoles[0].role.application);
+ user.institutionAdmin ? user.applications[0] : user.userRoles[0].role.applicationMaps[0]);
setManagementOption([providerOption]);
setRole({...role, applications: [providerOption]})
} else {
diff --git a/client/src/tabs/Roles.js b/client/src/tabs/Roles.js
index 5dff6f84..de8ffbf7 100644
--- a/client/src/tabs/Roles.js
+++ b/client/src/tabs/Roles.js
@@ -7,7 +7,7 @@ import {Button, ButtonSize, Chip, Loader} from "@surfnet/sds";
import {useNavigate} from "react-router-dom";
import {AUTHORITIES, highestAuthority, isUserAllowed, markAndFilterRoles} from "../utils/UserRole";
import {rolesByApplication, searchRoles} from "../api";
-import {isEmpty, stopEvent} from "../utils/Utils";
+import {distinctValues, isEmpty, stopEvent} from "../utils/Utils";
import debounce from "lodash.debounce";
import {chipTypeForUserRole} from "../utils/Authority";
import {ReactComponent as VoidImage} from "../icons/undraw_void_-3-ggu.svg";
@@ -19,6 +19,7 @@ export const Roles = () => {
const user = useAppStore(state => state.user);
const {roleSearchRequired} = useAppStore(state => state.config);
const navigate = useNavigate();
+
const [loading, setLoading] = useState(true);
const [searching, setSearching] = useState(false);
const [roles, setRoles] = useState([]);
@@ -34,7 +35,12 @@ export const Roles = () => {
} else {
rolesByApplication()
.then(res => {
- const newRoles = markAndFilterRoles(user, res, I18n.locale, I18n.t("roles.multiple"), I18n.t("forms.and"));
+ const newRoles = markAndFilterRoles(
+ user,
+ distinctValues(res, "id"),
+ I18n.locale,
+ I18n.t("roles.multiple"),
+ I18n.t("forms.and"));
setRoles(newRoles);
initFilterValues(newRoles);
setLoading(false);
@@ -55,11 +61,12 @@ export const Roles = () => {
value: allValue
}];
const reducedRoles = userRoles.reduce((acc, role) => {
- const option = acc.find(opt => opt.manageId === role.manageId);
+ const manageIdentifiers = role.applicationMaps.map(m => m.id);
+ const option = acc.find(opt => manageIdentifiers.includes(opt.manageId));
if (option) {
++option.nbr;
} else {
- acc.push({manageId: role.manageId, nbr: 1, name: role.applicationName})
+ role.applicationMaps.forEach(app => acc.push({manageId: app.id, nbr: 1, name: app[`name:${I18n.locale}`] || app[0]["name:en"]}))
}
return acc;
}, []);
@@ -199,7 +206,7 @@ export const Roles = () => {
)
}
const filteredRoles = filterValue.value === allValue ? roles :
- roles.filter(role => role.manageId=== filterValue.value);
+ roles.filter(role => role.applicationMaps.map(m => m.id).includes(filterValue.value));
return (
diff --git a/client/src/utils/Manage.js b/client/src/utils/Manage.js
index 9f434df2..83dce7dc 100644
--- a/client/src/utils/Manage.js
+++ b/client/src/utils/Manage.js
@@ -30,10 +30,14 @@ export const deriveApplicationAttributes = (role, locale, multiple, separator) =
if (!isEmpty(applications)) {
if (applications.length === 1) {
role.applicationName = applications[0][`name:${locale}`] || applications[0]["name:en"];
+ role.applicationNames = role.applicationName;
role.applicationOrganizationName = applications[0][`OrganizationName:${locale}`] || applications[0]["OrganizationName:en"];
role.logo = applications[0].logo;
} else {
role.applicationName = multiple;
+ const appNames = new Set(applications
+ .map(app => app[`name:${locale}`] || app["name:en"]));
+ role.applicationNames = splitListSemantically([...appNames], separator);
const orgNames = new Set(applications
.map(app => app[`OrganizationName:${locale}`] || app["OrganizationName:en"]));
role.applicationOrganizationName = splitListSemantically([...orgNames], separator);
diff --git a/client/src/utils/UserRole.js b/client/src/utils/UserRole.js
index 3a49f242..6f4580c6 100644
--- a/client/src/utils/UserRole.js
+++ b/client/src/utils/UserRole.js
@@ -97,7 +97,7 @@ export const allowedToRenewUserRole = (user, userRole) => {
}
}
-export const urnFromRole = (groupUrnPrefix, role) => `${groupUrnPrefix}:${role.manageId}:${role.shortName}`;
+export const urnFromRole = (groupUrnPrefix, role) => `${groupUrnPrefix}:${role.identifier}:${role.shortName}`;
export const markAndFilterRoles = (user, allRoles, locale, multiple, separator) => {
allRoles.forEach(role => {
@@ -121,6 +121,8 @@ export const markAndFilterRoles = (user, allRoles, locale, multiple, separator)
userRole.enforceEmailEquality = role.enforceEmailEquality;
userRole.applicationName = role.applicationName;
userRole.applicationOrganizationName = role.applicationOrganizationName;
+ userRole.applicationMaps = role.applicationMaps;
+ userRole.applications = role.applications;
userRole.logo = role.logo;
userRole.userRoleCount = role.userRoleCount;
})
diff --git a/client/src/utils/Utils.js b/client/src/utils/Utils.js
index cfcfbad0..042a43c7 100644
--- a/client/src/utils/Utils.js
+++ b/client/src/utils/Utils.js
@@ -39,3 +39,14 @@ export const sanitizeURL = url => {
const protocol = new URL(url).protocol;
return ["https:", "http:"].includes(protocol) ? url : "about:blank";
}
+
+export const distinctValues = (arr, attribute) => {
+ const distinctList = {};
+ return arr.filter(obj => {
+ if (distinctList[obj[attribute]]) {
+ return false;
+ }
+ distinctList[obj[attribute]] = true;
+ return true;
+ })
+}
diff --git a/server/pom.xml b/server/pom.xml
index 0b68ae4a..ceb24b64 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -107,17 +107,23 @@
azure-identity
1.9.1
-
- org.projectlombok
- lombok
- 1.18.30
-
commons-codec
commons-codec
1.16.0
+
+ org.openconext
+ java-crypto
+ 1.0.3
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+ provided
+
org.springframework.boot
spring-boot-starter-test
diff --git a/server/src/main/java/access/api/InvitationController.java b/server/src/main/java/access/api/InvitationController.java
index aaef5f0c..e05bcd0e 100644
--- a/server/src/main/java/access/api/InvitationController.java
+++ b/server/src/main/java/access/api/InvitationController.java
@@ -228,9 +228,12 @@ public ResponseEntity