Skip to content

Commit

Permalink
For now stop implementing multiple applications per role
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Nov 30, 2023
1 parent 0fde146 commit de5ace3
Show file tree
Hide file tree
Showing 33 changed files with 252 additions and 96 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ provisioning.local.json
IgnoreMeTest.java
dependency.tree
NOTES.txt
private_key_pkcs8.pem
12 changes: 11 additions & 1 deletion client/src/__tests__/utils/Utils.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
2 changes: 1 addition & 1 deletion client/src/locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion client/src/locale/nl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/Role.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export const Role = () => {
<a href={role.landingPage}
rel="noreferrer"
target="_blank">
<span className={"application-name"}>{`${role.applicationName}`}</span>
<span className={"application-name"}>{`${role.applicationNames}`}</span>
</a>{role.applicationOrganizationName && <span>{` (${role.applicationOrganizationName})`}</span>}
</div>

Expand Down
12 changes: 7 additions & 5 deletions client/src/pages/RoleForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
17 changes: 12 additions & 5 deletions client/src/tabs/Roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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([]);
Expand All @@ -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);
Expand All @@ -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;
}, []);
Expand Down Expand Up @@ -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 (
<div className={"mod-roles"}>
Expand Down
4 changes: 4 additions & 0 deletions client/src/utils/Manage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion client/src/utils/UserRole.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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;
})
Expand Down
11 changes: 11 additions & 0 deletions client/src/utils/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
})
}
16 changes: 11 additions & 5 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,23 @@
<artifactId>azure-identity</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
<dependency>
<groupId>org.openconext</groupId>
<artifactId>java-crypto</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
5 changes: 4 additions & 1 deletion server/src/main/java/access/api/InvitationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,12 @@ public ResponseEntity<Map<String, String>> accept(@Validated @RequestBody Accept
.filter(userRole -> userRole.getRole().getId().equals(role.getId())).findFirst();
if (optionalUserRole.isPresent()) {
UserRole userRole = optionalUserRole.get();
if (!userRole.getAuthority().hasEqualOrHigherRights(invitation.getIntendedAuthority())) {
if (invitation.getIntendedAuthority().hasHigherRights(userRole.getAuthority())) {
userRole.setAuthority(invitation.getIntendedAuthority());
userRole.setEndDate(invitation.getRoleExpiryDate());
if (invitation.getIntendedAuthority().equals(Authority.GUEST)) {
userRole.setGuestRoleIncluded(true);
}
}
} else {
UserRole userRole = new UserRole(
Expand Down
9 changes: 8 additions & 1 deletion server/src/main/java/access/api/RoleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.springframework.web.bind.annotation.*;

import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME;
Expand Down Expand Up @@ -157,12 +158,18 @@ private ResponseEntity<Role> saveOrUpdate(Role role, User user) {
UserPermissions.assertManagerRole(role.getApplicationMaps(), user);

boolean isNew = role.getId() == null;
AtomicReference<Role> roleAtomicReference = new AtomicReference<>();
if (!isNew) {
role.setShortName(roleRepository.findById(role.getId()).orElseThrow(NotFoundException::new).getShortName());
Role previousRole = roleRepository.findById(role.getId()).orElseThrow(NotFoundException::new);
//We don't allow shortName changes after creation
role.setShortName(previousRole.getShortName());
roleAtomicReference.set(previousRole);
}
Role saved = roleRepository.save(role);
if (isNew) {
provisioningService.newGroupRequest(saved);
} else {
provisioningService.updateGroupRequest(roleAtomicReference.get(), saved);
}
AccessLogger.role(LOG, isNew ? Event.Created : Event.Updated, user, role);

Expand Down
4 changes: 3 additions & 1 deletion server/src/main/java/access/api/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import access.security.UserPermissions;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import crypto.KeyStore;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -65,6 +66,7 @@ public UserController(Config config,
Manage manage,
ObjectMapper objectMapper,
RemoteProvisionedUserRepository remoteProvisionedUserRepository,
KeyStore keyStore,
@Value("${config.eduid-idp-schac-home-organization}") String eduidIdpSchacHomeOrganization,
@Value("${config.server-url}") String serverBaseURL,
@Value("${voot.group_urn_domain}") String groupUrnPrefix) {
Expand All @@ -74,7 +76,7 @@ public UserController(Config config,
this.objectMapper = objectMapper;
this.manage = manage;
this.remoteProvisionedUserRepository = remoteProvisionedUserRepository;
this.graphClient = new GraphClient(serverBaseURL, eduidIdpSchacHomeOrganization);
this.graphClient = new GraphClient(serverBaseURL, eduidIdpSchacHomeOrganization, keyStore);
}

@GetMapping("config")
Expand Down
11 changes: 6 additions & 5 deletions server/src/main/java/access/manage/LocalManage.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -70,7 +68,10 @@ public Map<String, Object> providerById(EntityType entityType, String id) {
}

@Override
public List<Map<String, Object>> provisioning(List<String> ids) {
public List<Map<String, Object>> provisioning(Collection<String> ids) {
if (CollectionUtils.isEmpty(ids)) {
return Collections.emptyList();
}
return providers(EntityType.PROVISIONING).stream()
.filter(map -> {
List<Map<String, String>> applications = (List<Map<String, String>>) map.get("applications");
Expand Down
3 changes: 2 additions & 1 deletion server/src/main/java/access/manage/Manage.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public interface Manage {

List<Map<String, Object>> providersByIdIn(EntityType entityType, List<String> identifiers);

List<Map<String, Object>> provisioning(List<String> ids);
List<Map<String, Object>> provisioning(Collection<String> ids);

List<Map<String, Object>> providersByInstitutionalGUID(String organisationGUID);

Expand Down Expand Up @@ -71,6 +71,7 @@ default Map<String, Object> transformProvider(Map<String, Object> provider) {
application.put("applications", data.get("applications"));
application.put("entityid", data.get("entityid"));
application.put("logo", metaDataFields.get("logo:0:url"));
application.put("url", metaDataFields.get("coin:application_url"));
application.put("OrganizationName:en", metaDataFields.get("OrganizationName:en"));
application.put("OrganizationName:nl", metaDataFields.get("OrganizationName:nl"));
application.put("name:en", metaDataFields.get("name:en"));
Expand Down
12 changes: 11 additions & 1 deletion server/src/main/java/access/manage/ManageConf.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package access.manage;


import com.fasterxml.jackson.databind.ObjectMapper;
import crypto.KeyStore;
import crypto.RSAKeyStore;
import io.micrometer.core.instrument.util.IOUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;

import java.io.IOException;

@Configuration
public class ManageConf {

Expand All @@ -20,5 +24,11 @@ public Manage manage(@Value("${manage.url}") String url,
return enabled ? new RemoteManage(url, user, password, objectMapper) : new LocalManage(objectMapper, local);
}

@Bean
public KeyStore keyStore(@Value("${crypto.development-mode}") Boolean developmentMode,
@Value("${crypto.private-key-location}") Resource privateKey) throws IOException {
return developmentMode ? new RSAKeyStore() : new RSAKeyStore(IOUtils.toString(privateKey.getInputStream()));
}


}
Loading

0 comments on commit de5ace3

Please sign in to comment.