Skip to content

Commit

Permalink
Many-to-many roles-applica†ions
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Nov 26, 2023
1 parent d06a1b2 commit 8868a8b
Show file tree
Hide file tree
Showing 31 changed files with 158 additions and 118 deletions.
3 changes: 3 additions & 0 deletions client/src/components/Logo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export default function Logo({src, className = "", alt = ""}) {
if (isEmpty(src)) {
return <NotFoundIcon/>;
}
if (typeof src !== "string") {
return src;
}
const urlSrc = srcUrl(src, "jpeg");
return <img src={urlSrc} alt={alt} className={className}/>

Expand Down
2 changes: 1 addition & 1 deletion client/src/components/SelectField.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
border: 1px solid var(--sds--color--gray--300);
border-radius: $br;
font-size: 16px;
height: 48px;
min-height: 48px;

&.creatable {
height: auto;
Expand Down
5 changes: 5 additions & 0 deletions client/src/icons/multi-role.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/src/locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const en = {
applicationName: "Application",
roleDetails: "Role details",
invitationDetails: "Invitation details",
multiple: "Multiple applications",
accessRole: "Name",
name: "Name",
namePlaceHolder: "The name of the role",
Expand Down
1 change: 1 addition & 0 deletions client/src/locale/nl.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const nl = {
applicationName: "Applicatie",
roleDetails: "Rol details",
invitationDetails: "Uitnodiging details",
multiple: "Meerdere applicaties",
accessRole: "Naam",
name: "Naam",
namePlaceHolder: "Naam van de rol",
Expand Down
4 changes: 2 additions & 2 deletions client/src/pages/InvitationForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ export const InvitationForm = () => {
if (isUserAllowed(AUTHORITIES.MANAGER, user)) {
rolesByApplication()
.then(res => {
const markedRoles = markAndFilterRoles(user, res, I18n.locale);
const markedRoles = markAndFilterRoles(user, res, I18n.locale, I18n.t("roles.multiple"));
setInitialRole(markedRoles);
setRoles(markedRoles);
setLoading(false);
})
} else {
const markedRoles = markAndFilterRoles(user, [], I18n.locale);
const markedRoles = markAndFilterRoles(user, [], I18n.locale, I18n.t("roles.multiple"));
setInitialRole(markedRoles);
setRoles(markedRoles)
setLoading(false);
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 @@ -58,7 +58,7 @@ export const Role = () => {
}
Promise.all([roleByID(id, false), userRolesByRoleId(id), invitationsByRoleId(id)])
.then(res => {
deriveApplicationAttributes(res[0])
deriveApplicationAttributes(res[0], I18n.locale, I18n.t("roles.multiple"))
setRole(res[0]);
setUserRole(res[1].find(userRole => userRole.role.id === res[0].id && userRole.userInfo.id === user.id));
const newTabs = [
Expand Down
40 changes: 16 additions & 24 deletions client/src/pages/RoleForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {useNavigate, useParams} from "react-router-dom";
import {useAppStore} from "../stores/AppStore";
import I18n from "../locale/I18n";
import {AUTHORITIES, isUserAllowed, urnFromRole} from "../utils/UserRole";
import {allProviders, createRole, deleteRole, me, roleByID, shortNameExists, updateRole, validate} from "../api";
import {allProviders, createRole, deleteRole, me, roleByID, updateRole, validate} from "../api";
import {Button, ButtonType, Checkbox, Loader} from "@surfnet/sds";
import "./RoleForm.scss";
import {UnitHeader} from "../components/UnitHeader";
Expand All @@ -23,17 +23,17 @@ export const RoleForm = () => {
const {id} = useParams();
const nameRef = useRef();

const required = ["name", "description", "applications"];
const {user, setFlash, config} = useAppStore(state => state);

const [role, setRole] = useState({name: "", shortName: "", defaultExpiryDays: 0});
const [providers, setProviders] = useState([]);
const [isNewRole, setNewRole] = useState(true);
const [loading, setLoading] = useState(true);
const [initial, setInitial] = useState(true);
const required = ["name", "description", "manageId"];
const [alreadyExists, setAlreadyExists] = useState({});
const [invalidValues, setInvalidValues] = useState({});
const [managementOption, setManagementOption] = useState({});
const [managementOption, setManagementOption] = useState([]);
const [confirmation, setConfirmation] = useState({});
const [confirmationOpen, setConfirmationOpen] = useState(false);

Expand Down Expand Up @@ -70,11 +70,11 @@ export const RoleForm = () => {
if (newRole) {
const providerOption = singleProviderToOption(user.superUser ? res[0][0] :
user.institutionAdmin ? user.applications[0] : user.userRoles[0].role.application);
setManagementOption(providerOption);
setRole({...role, manageId: providerOption.value, manageType: providerOption.type.toUpperCase()})
setManagementOption([providerOption]);
setRole({...role, applications: [providerOption]})
} else {
breadcrumbPath.push({path: `/roles/${res[0].id}`, value: name});
setManagementOption(singleProviderToOption(res[0].application));
setManagementOption([singleProviderToOption(res[0].application)]);
}
breadcrumbPath.push({value: I18n.t(`roles.${newRole ? "new" : "edit"}`, {name: name})});
useAppStore.setState({breadcrumbPath: breadcrumbPath});
Expand All @@ -89,14 +89,6 @@ export const RoleForm = () => {
},
[id]); // eslint-disable-line react-hooks/exhaustive-deps

const validateShortName = (shortName, manageId) => {
if (!isEmpty(manageId) && !isEmpty(shortName)) {
shortNameExists(shortName, manageId, role.id)
.then(json => setAlreadyExists({...alreadyExists, shortName: json.exists}));
}
return true;
}

const validateValue = (type, attribute, value) => {
if (!isEmpty(value)) {
validate(type, value)
Expand Down Expand Up @@ -189,11 +181,6 @@ export const RoleForm = () => {
value={role.name || ""}
placeholder={I18n.t("roles.namePlaceHolder")}
error={alreadyExists.name || (!initial && isEmpty(role.name))}
onBlur={e => {
if (isNewRole) {
validateShortName(constructShortName(e.target.value), role.manageId);
}
}}
onRef={nameRef}
onChange={e => {
const shortName = isNewRole ? constructShortName(e.target.value) : role.shortName;
Expand Down Expand Up @@ -234,17 +221,22 @@ export const RoleForm = () => {

<SelectField name={I18n.t("roles.manage")}
value={managementOption}
options={options.filter(provider => provider.value !== managementOption.value)}
onChange={option => {
setManagementOption(option);
setRole({...role, manageId: option.value, manageType: option.type.toUpperCase()});
validateShortName(constructShortName(role.name), option.value);
options={options.filter(option => !managementOption.some(provider => option.value === provider.value))}
onChange={options => {
setManagementOption(options);
setRole({...role, applications: options});
}}
searchable={true}
clearable={false}
isMulti={true}
disabled={options.length === 1}
toolTip={I18n.t("tooltips.manageService")}
/>
{(!initial && isEmpty(role.applications)) &&
<ErrorIndicator msg={I18n.t("forms.required", {
attribute: I18n.t("roles.manage").toLowerCase()
})}/>}

<InputField name={I18n.t("roles.landingPage")}
value={role.landingPage || ""}
isUrl={true}
Expand Down
8 changes: 4 additions & 4 deletions client/src/tabs/Roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ export const Roles = () => {
} else {
rolesByApplication()
.then(res => {
const newRoles = markAndFilterRoles(user, res, I18n.locale);
const newRoles = markAndFilterRoles(user, res, I18n.locale, I18n.t("roles.multiple"));
setRoles(newRoles);
initFilterValues(newRoles);
setLoading(false);
})
}
} else {
const newRoles = markAndFilterRoles(user, [], I18n.locale);
const newRoles = markAndFilterRoles(user, [], I18n.locale, I18n.t("roles.multiple"));
setRoles(newRoles);
initFilterValues(newRoles);
setLoading(false);
Expand Down Expand Up @@ -100,7 +100,7 @@ export const Roles = () => {
const delayedAutocomplete = debounce(query => {
searchRoles(query)
.then(res => {
setRoles(markAndFilterRoles(user, res, I18n.locale));
setRoles(markAndFilterRoles(user, res, I18n.locale, I18n.t("roles.multiple")));
setMoreToShow(res.length === 15);
setNoResults(res.length === 0);
setSearching(false);
Expand Down Expand Up @@ -151,7 +151,7 @@ export const Roles = () => {
header: "",
mapper: role => {
return <div className="role-icon">
<img src={role.logo} alt="logo"/>
{typeof role.logo === "string" ? <img src={role.logo} alt="logo"/> :role.logo}
</div>
}
},
Expand Down
25 changes: 18 additions & 7 deletions client/src/utils/Manage.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import {isEmpty} from "./Utils";
import {ReactComponent as MultipleIcon} from "../icons/multi-role.svg";

export const singleProviderToOption = provider => {
const organisation = provider["OrganizationName:en"];
const organisationValue = isEmpty(organisation) ? "" : ` (${organisation})`;
return {
value: provider.id,
label: `${provider["name:en"]}${organisationValue}`,
type: provider.type
type: provider.type.toUpperCase(),
manageType: provider.type.toUpperCase(),
manageId: provider.id
};
}

export const providersToOptions = providers => {
return providers.map(provider => singleProviderToOption(provider));
}

export const deriveApplicationAttributes = (role, locale) => {
const application = role.application;
if (!isEmpty(application)) {
role.applicationName = application[`name:${locale}`] || application["name:en"]
role.applicationOrganizationName = application[`OrganizationName:${locale}`] || application["OrganizationName:en"];
role.logo = application.logo;
export const deriveApplicationAttributes = (role, locale, multiple) => {
const applications = role.applicationMaps;
if (!isEmpty(applications)) {
if (applications.length === 1) {
role.applicationName = applications[0][`name:${locale}`] || applications[0]["name:en"];
role.applicationOrganizationName = applications[0][`OrganizationName:${locale}`] || applications[0]["OrganizationName:en"];
role.logo = applications[0].logo;
} else {
role.applicationName = multiple;
role.applicationOrganizationName = applications
.map(app => app[`OrganizationName:${locale}`] || app["OrganizationName:en"])
.join(", ")
role.logo = <MultipleIcon/>;
}
}
}

Expand Down
7 changes: 4 additions & 3 deletions client/src/utils/UserRole.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {isEmpty} from "./Utils";
import {deriveApplicationAttributes} from "./Manage";
import I18n from "../locale/I18n";

export const INVITATION_STATUS = {
OPEN: "OPEN",
Expand Down Expand Up @@ -99,18 +100,18 @@ export const allowedToRenewUserRole = (user, userRole) => {

export const urnFromRole = (groupUrnPrefix, role) => `${groupUrnPrefix}:${role.manageId}:${role.shortName}`;

export const markAndFilterRoles = (user, allRoles, locale) => {
export const markAndFilterRoles = (user, allRoles, locale, multiple) => {
allRoles.forEach(role => {
role.isUserRole = false;
role.label = role.name;
role.value = role.id;
deriveApplicationAttributes(role, locale);
deriveApplicationAttributes(role, locale, multiple);
});
const userRoles = user.userRoles;
userRoles.forEach(userRole => {
userRole.isUserRole = true;
const role = userRole.role;
deriveApplicationAttributes(role, locale);
deriveApplicationAttributes(role, locale, multiple);
userRole.name = role.name;
userRole.label = role.name;
userRole.value = role.id;
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/java/access/api/HasManage.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ default List<GroupedProviders> getGroupedProviders(List<Role> requestedRoles) {
.collect(Collectors.toSet())
.stream()
.map(manageIdentifier -> {
Map<String, Object> provider = getManage().providerById(manageIdentifier.entityType(), manageIdentifier.id());
Map<String, Object> provider = getManage().providerById(manageIdentifier.manageType(), manageIdentifier.manageId());
String id = (String) provider.get("id");
return new GroupedProviders(
provider,
Expand Down
16 changes: 5 additions & 11 deletions server/src/main/java/access/api/ManageController.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;

import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME;
Expand Down Expand Up @@ -76,19 +73,16 @@ public ResponseEntity<List<Map<String, Object>>> providers(@Parameter(hidden = t
@GetMapping("applications")
public ResponseEntity<Map<String, List<Map<String, Object>>>> applications(@Parameter(hidden = true) User user) {
UserPermissions.assertSuperUser(user);
List<ManageIdentifier> manageIdentifiers = roleRepository.findDistinctManageIdentifiers().stream()
.map(tuple -> new ManageIdentifier(tuple[0].replaceAll("[\"\\]\\[]", ""),
EntityType.valueOf(tuple[1].replaceAll("[\"\\]\\[]", ""))))
.toList();
Map<EntityType, List<ManageIdentifier>> groupedByManageType = manageIdentifiers.stream().collect(Collectors.groupingBy(ManageIdentifier::entityType));
Set<ManageIdentifier> manageIdentifiers = roleRepository.findDistinctManageIdentifiers();
Map<EntityType, List<ManageIdentifier>> groupedByManageType = manageIdentifiers.stream().collect(Collectors.groupingBy(ManageIdentifier::manageType));
List<Map<String, Object>> providers = groupedByManageType.entrySet().stream()
.map(entry -> manage.providersByIdIn(
entry.getKey(),
entry.getValue().stream().map(ManageIdentifier::id).collect(Collectors.toList())))
entry.getValue().stream().map(ManageIdentifier::manageId).collect(Collectors.toList())))
.flatMap(Collection::stream)
.toList();
List<Map<String, Object>> provisionings = manage.provisioning(manageIdentifiers.stream()
.map(ManageIdentifier::id)
.map(ManageIdentifier::manageId)
.toList());
return ResponseEntity.ok(Map.of(
"providers", providers,
Expand Down
1 change: 1 addition & 0 deletions server/src/main/java/access/api/RoleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public ResponseEntity<Role> newRole(@Validated @RequestBody Role role, @Paramete
throw new NotAllowedException(
String.format("Duplicate name: '%s' for manage entity:'%s'", shortName, application.getManageId()));
}
role.setIdentifier(UUID.randomUUID().toString());
return saveOrUpdate(role, user);
}

Expand Down
9 changes: 4 additions & 5 deletions server/src/main/java/access/config/ApplicationConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@

public class ApplicationConverter implements AttributeConverter<Set<Application>, String> {

private final static ObjectMapper objectMapper = new ObjectMapper();

@SneakyThrows
@Override
public String convertToDatabaseColumn(Set<Application> attribute) {
return objectMapper.writeValueAsString(attribute);
return ObjectMapperHolder.objectMapper.writeValueAsString(attribute);
}

@SneakyThrows
@Override
@SuppressWarnings("unchecked")
public Set<Application> convertToEntityAttribute(String dbData) {
Set<Map<String, String>> set = objectMapper.readValue(dbData, Set.class);
return set.stream().map(m -> new Application(m.get("manageId"), EntityType.valueOf(m.get("manageType"))))
Set<Map<String, String>> applications = ObjectMapperHolder.objectMapper.readValue(dbData, Set.class);
return applications.stream()
.map(m -> new Application(m.get("manageId"), EntityType.valueOf(m.get("manageType"))))
.collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@
import org.springframework.context.annotation.Primary;

@Configuration
public class JacksonConfiguration {
public class ObjectMapperHolder {

public static final ObjectMapper objectMapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.registerModule(new Jdk8Module());

@Bean
@Primary
public ObjectMapper objectMapper() {
return new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.registerModule(new Jdk8Module());
return objectMapper;
}

}
Expand Down
3 changes: 2 additions & 1 deletion server/src/main/java/access/manage/LocalManage.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ public List<Map<String, Object>> providers(EntityType... entityTypes) {

@Override
public List<Map<String, Object>> providersByIdIn(EntityType entityType, List<String> identifiers) {
return transformProvider(this.allProviders.get(entityType).stream()
List<Map<String, Object>> providers = this.allProviders.get(entityType);
return transformProvider(providers.stream()
.filter(provider -> identifiers.contains(provider.get("_id")))
.collect(Collectors.toList()));
}
Expand Down
Loading

0 comments on commit 8868a8b

Please sign in to comment.