Skip to content

Commit

Permalink
WWIP for landingpage per application per role
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Dec 13, 2023
1 parent 5595b0b commit d47f86f
Show file tree
Hide file tree
Showing 21 changed files with 319 additions and 76 deletions.
2 changes: 1 addition & 1 deletion client/src/locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ const en = {
inviter: "Send invitations to persons who will - once accepted - become guest users for the application",
overrideSettingsAllowed: "If checked then invitations for this role can't override the advanced settings (e.g. email equality, eduID only and the role expiry end date)",
removeUserRole: "Remove all selected user roles",
guestRoleIncludedTooltip: "Do you also want to invite the invitees as guests when they accept the invitation?",
guestRoleIncludedTooltip: "Do you also want to grant the invitees the guest role when they accept the invitation?",
},
confirmationDialog: {
title: "Confirm",
Expand Down
69 changes: 46 additions & 23 deletions client/src/pages/InvitationForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {ReactComponent as UserIcon} from "@surfnet/sds/icons/functional-icons/id
import {ReactComponent as UpIcon} from "@surfnet/sds/icons/functional-icons/arrow-up-2.svg";
import {ReactComponent as DownIcon} from "@surfnet/sds/icons/functional-icons/arrow-down-2.svg";
import {newInvitation, rolesByApplication} from "../api";
import {Button, ButtonType, Checkbox, Loader, RadioOptions, Tooltip} from "@surfnet/sds";
import {Button, ButtonType, Loader, RadioOptions, Switch, Tooltip} from "@surfnet/sds";
import "./InvitationForm.scss";
import {UnitHeader} from "../components/UnitHeader";
import InputField from "../components/InputField";
Expand Down Expand Up @@ -268,37 +268,60 @@ export const InvitationForm = () => {
{I18n.t("roles.hideAdvancedSettings")}
<UpIcon/>
</a>
<Checkbox name={I18n.t("invitations.enforceEmailEquality")}
value={invitation.enforceEmailEquality || false}
onChange={e => setInvitation({...invitation, enforceEmailEquality: e.target.checked})}
info={I18n.t("invitations.enforceEmailEquality")}
readOnly={selectedRoles.some(role => !role.overrideSettingsAllowed)}
tooltip={I18n.t("tooltips.enforceEmailEqualityTooltip")}
/>

<Checkbox name={I18n.t("invitations.eduIDOnly")}
value={invitation.eduIDOnly || false}
onChange={e => setInvitation({...invitation, eduIDOnly: e.target.checked})}
info={I18n.t("invitations.eduIDOnly")}
readOnly={selectedRoles.some(role => !role.overrideSettingsAllowed)}
tooltip={I18n.t("tooltips.eduIDOnlyTooltip")}
/>
{selectedRoles.every(role => role.overrideSettingsAllowed) &&
<>
<div className="switch-container">
<div className={"inner-switch"}>
<span
className="switch-label">{I18n.t("invitations.enforceEmailEquality")}</span>
<span
className="switch-info">{I18n.t("tooltips.enforceEmailEqualityTooltip")}</span>
</div>
<Switch name={"enforceEmailEquality"}
value={invitation.enforceEmailEquality || false}
onChange={val => setInvitation({
...invitation,
enforceEmailEquality: val
})}/>
</div>

<Checkbox name={I18n.t("invitations.guestRoleIncluded")}
value={invitation.guestRoleIncluded || false}
onChange={e => setInvitation({...invitation, guestRoleIncluded: e.target.checked})}
info={I18n.t("invitations.guestRoleIncluded")}
readOnly={invitation.intendedAuthority === AUTHORITIES.GUEST}
tooltip={I18n.t("tooltips.guestRoleIncludedTooltip")}
/>
</>}

{selectedRoles.every(role => role.overrideSettingsAllowed) &&
<>
<div className="switch-container">
<div className={"inner-switch"}>
<span className="switch-label">{I18n.t("invitations.eduIDOnly")}</span>
<span className="switch-info">{I18n.t("tooltips.eduIDOnlyTooltip")}</span>
</div>
<Switch name={"eduIDOnly"}
value={invitation.eduIDOnly || false}
onChange={val => setInvitation({...invitation, eduIDOnly: val})}/>
</div>

</>}

{invitation.intendedAuthority === AUTHORITIES.GUEST && <>
<div className="switch-container">
<div className={"inner-switch"}>
<span className="switch-label">{I18n.t("invitations.guestRoleIncluded")}</span>
<span className="switch-info">{I18n.t("tooltips.guestRoleIncludedTooltip")}</span>
</div>
<Switch name={"guestRoleIncluded"}
value={invitation.guestRoleIncluded || false}
onChange={val => setInvitation({...invitation, guestRoleIncluded: val})}/>
</div>
</>}
{selectedRoles.every(role => role.overrideSettingsAllowed) &&
<RadioOptions name={"roleExpiryDate"}
value={customRoleExpiryDate}
onChange={e => setCustomRoleExpiryDate(!customRoleExpiryDate)}
label={I18n.t("invitations.roleExpiryDateQuestion")}
falseLabel={I18n.t("forms.no")}
reverse={false}
trueLabel={I18n.t("forms.specificDate")}
/>
/>}
{customRoleExpiryDate &&
<DateField value={invitation.roleExpiryDate}
onChange={e => setInvitation({...invitation, roleExpiryDate: e})}
Expand Down
31 changes: 30 additions & 1 deletion client/src/pages/InvitationForm.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,37 @@
margin-left: 15px;
}
}

.switch-container {
display: flex;
margin-top: 20px;
align-items: center;


.sds--tooltip-parent {
margin-left: auto;
}

.inner-switch {
display: flex;
flex-direction: column;
margin-right: 20px;
}

.switch-label {
font-weight: 600;
}

}

.switch-info {
font-size: 14px;
color: var(--sds--color--gray--500);
}


.sds--radio-options {
width: 320px;
width: 380px;

.sds--text-field-container div:last-child {
margin-left: auto;
Expand Down
38 changes: 26 additions & 12 deletions server/src/main/java/access/api/RoleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
import access.logging.AccessLogger;
import access.logging.Event;
import access.manage.Manage;
import access.model.Application;
import access.model.Authority;
import access.model.Role;
import access.model.User;
import access.model.*;
import access.provision.ProvisioningService;
import access.provision.scim.GroupURN;
import access.repository.ApplicationRepository;
import access.repository.ApplicationUsageRepository;
import access.repository.RoleRepository;
import access.security.UserPermissions;
import access.validation.URLFormatValidator;
Expand Down Expand Up @@ -47,17 +45,21 @@ public class RoleController {
private final Config config;
private final RoleRepository roleRepository;
private final ApplicationRepository applicationRepository;
private final ApplicationUsageRepository applicationUsageRepository;
private final Manage manage;
private final ProvisioningService provisioningService;
private final URLFormatValidator urlFormatValidator = new URLFormatValidator();

public RoleController(Config config,
RoleRepository roleRepository,
ApplicationRepository applicationRepository, Manage manage,
ApplicationRepository applicationRepository,
ApplicationUsageRepository applicationUsageRepository,
Manage manage,
ProvisioningService provisioningService) {
this.config = config;
this.roleRepository = roleRepository;
this.applicationRepository = applicationRepository;
this.applicationUsageRepository = applicationUsageRepository;
this.manage = manage;
this.provisioningService = provisioningService;
}
Expand All @@ -73,7 +75,7 @@ public ResponseEntity<List<Role>> rolesByApplication(@Parameter(hidden = true) U
if (user.isInstitutionAdmin()) {
Set<String> manageIdentifiers = user.getApplications().stream().map(m -> (String) m.get("id")).collect(Collectors.toSet());
//This is a shortcoming of the json_array
manageIdentifiers.forEach(manageId -> roles.addAll(roleRepository.findByApplicationsManageId(manageId)));
manageIdentifiers.forEach(manageId -> roles.addAll(roleRepository.findByApplicationUsagesApplicationManageId(manageId)));
}
Set<String> manageIdentifiers = user.getUserRoles().stream()
//If the user has an userRole as Inviter, then we must exclude those
Expand All @@ -82,7 +84,7 @@ public ResponseEntity<List<Role>> rolesByApplication(@Parameter(hidden = true) U
.flatMap(Collection::stream)
.map(Application::getManageId)
.collect(Collectors.toSet());
manageIdentifiers.forEach(manageId -> roles.addAll(roleRepository.findByApplicationsManageId(manageId)));
manageIdentifiers.forEach(manageId -> roles.addAll(roleRepository.findByApplicationUsagesApplicationManageId(manageId)));
return ResponseEntity.ok(manage.addManageMetaData(roles));
}

Expand Down Expand Up @@ -147,18 +149,30 @@ private ResponseEntity<Role> saveOrUpdate(Role role, User user) {
previousManageIdentifiersReference.set(previousRole.applicationIdentifiers());
}
//This is the disadvantage of having to save references from Manage
role.setApplications(role.getApplications().stream()
.map(application -> applicationRepository.findByManageIdAndManageType(application.getManageId(), application.getManageType())
.orElseGet(() -> applicationRepository.save(application)))
.collect(Collectors.toSet()));
Set<Application> applications = role.getApplications();
Set<ApplicationUsage> applicationUsages = applications.stream()
.map(applicationFromClient -> {
Optional<Application> optionalApplication = applicationRepository
.findByManageIdAndManageType(applicationFromClient.getManageId(), applicationFromClient.getManageType());
Application applicationFromDB = optionalApplication
.orElseGet(() -> applicationRepository.save(applicationFromClient));
ApplicationUsage applicationUsage = applicationUsageRepository.findByRoleIdAndApplicationManageIdAndApplicationManageType(
role.getId(),
applicationFromDB.getManageId(),
applicationFromDB.getManageType()
).orElseGet(() -> new ApplicationUsage(applicationFromDB, applicationFromClient.getLandingPage()));
applicationUsage.setLandingPage(applicationFromClient.getLandingPage());
return applicationUsage;
})
.collect(Collectors.toSet());
role.setApplicationUsages(applicationUsages);
Role saved = roleRepository.save(role);
if (isNew) {
provisioningService.newGroupRequest(saved);
} else {
provisioningService.updateGroupRequest(previousManageIdentifiersReference.get(), saved);
}
AccessLogger.role(LOG, isNew ? Event.Created : Event.Updated, user, role);

return ResponseEntity.ok(saved);
}

Expand Down
8 changes: 5 additions & 3 deletions server/src/main/java/access/model/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ public class Application implements Serializable {
@NotNull
private EntityType manageType;

@Column(name = "landing_page")
@Transient
private String landingPage;

@ManyToMany(mappedBy = "applications")
@OneToMany(mappedBy = "application",
fetch = FetchType.LAZY,
orphanRemoval = true)
@JsonIgnore
private Set<Role> roles = new HashSet<>();
private Set<ApplicationUsage> applicationUsages = new HashSet<>();

public Application(String manageId, EntityType manageType) {
this.manageId = manageId;
Expand Down
37 changes: 37 additions & 0 deletions server/src/main/java/access/model/ApplicationUsage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package access.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.io.Serializable;

@Entity(name = "application_usages")
@NoArgsConstructor
@Getter
@Setter
public class ApplicationUsage implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "landing_page")
private String landingPage;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id")
@JsonIgnore
private Role role;

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "application_id")
private Application application;

public ApplicationUsage(Application application, String landingPage) {
this.landingPage = landingPage;
this.application = application;
}
}
38 changes: 26 additions & 12 deletions server/src/main/java/access/model/Role.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;

@Entity(name = "roles")
@NoArgsConstructor
Expand Down Expand Up @@ -65,13 +66,11 @@ public class Role implements Serializable, Provisionable {
@Formula(value = "(SELECT COUNT(*) FROM user_roles ur WHERE ur.role_id=id)")
private Long userRoleCount;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "roles_applications",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "application_id"))
//TODO https://www.baeldung.com/hibernate-persisting-maps, store landingpage per applications
private Set<Application> applications = new HashSet<>();
@OneToMany(mappedBy = "role",
fetch = FetchType.EAGER,
orphanRemoval = true,
cascade = CascadeType.ALL)
private Set<ApplicationUsage> applicationUsages = new HashSet<>();

@OneToMany(mappedBy = "role",
orphanRemoval = true,
Expand All @@ -86,22 +85,25 @@ public class Role implements Serializable, Provisionable {
@Transient
private List<Map<String, Object>> applicationMaps;

@Transient
private Set<Application> applications;

public Role(String name,
String description,
String landingPage,
Set<Application> applications,
Set<ApplicationUsage> applicationUsages,
Integer defaultExpiryDays,
boolean enforceEmailEquality,
boolean eduIDOnly) {
this(name, GroupURN.sanitizeRoleShortName(name), description, landingPage, applications,
this(name, GroupURN.sanitizeRoleShortName(name), description, landingPage, applicationUsages,
defaultExpiryDays, enforceEmailEquality, eduIDOnly, Collections.emptyList());
}

public Role(@NotNull String name,
@NotNull String shortName,
String description,
String landingPage,
Set<Application> applications,
Set<ApplicationUsage> applicationUsages,
Integer defaultExpiryDays,
boolean enforceEmailEquality,
boolean eduIDOnly,
Expand All @@ -113,15 +115,27 @@ public Role(@NotNull String name,
this.defaultExpiryDays = defaultExpiryDays;
this.enforceEmailEquality = enforceEmailEquality;
this.eduIDOnly = eduIDOnly;
this.applications = applications;
this.applicationUsages = applicationUsages;
this.applicationUsages.forEach(applicationUsage -> applicationUsage.setRole(this));
this.applicationMaps = applicationMaps;
this.identifier = UUID.randomUUID().toString();
}

@Transient
@JsonIgnore
public List<String> applicationIdentifiers() {
return applications.stream().map(Application::getManageId).toList();
return applicationUsages.stream()
.map(applicationUsage -> applicationUsage.getApplication().getManageId()).toList();
}

@Transient
public Set<Application> getApplications() {
return applicationUsages.stream()
.map(ApplicationUsage::getApplication).collect(Collectors.toSet());
}

public void setApplicationUsages(Set<ApplicationUsage> applicationUsages) {
this.applicationUsages = applicationUsages;
this.applicationUsages.forEach(applicationUsage -> applicationUsage.setRole(this));
}
}
4 changes: 2 additions & 2 deletions server/src/main/java/access/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ public void removeUserRole(UserRole role) {
public Set<ManageIdentifier> manageIdentifierSet() {
return userRoles.stream()
.filter(userRole -> userRole.getAuthority().equals(Authority.GUEST))
.map(userRole -> userRole.getRole().getApplications())
.map(userRole -> userRole.getRole().getApplicationUsages())
.flatMap(Collection::stream)
.map(application -> new ManageIdentifier(application.getManageId(), application.getManageType()))
.map(applicationUsage -> new ManageIdentifier(applicationUsage.getApplication().getManageId(),applicationUsage.getApplication().getManageType()))
.collect(Collectors.toSet());
}

Expand Down
Loading

0 comments on commit d47f86f

Please sign in to comment.