Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,17 @@ To build and deploy (the latter requires credentials in your maven settings):
```bash
mvn clean deploy
```

## [Upgrade](#upgrade)

To check the pom.xml with the latest versions, run
```
cd server
mvn versions:display-dependency-updates -DprocessDependencyManagement=false -DdependencyIncludes=*:*
```
To see the latest versions report for the client run
```
cd client
nvm use
yarn outdated
```
4 changes: 3 additions & 1 deletion client/src/pages/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {Footer} from "../components/Footer";
import {BreadCrumb} from "../components/BreadCrumb";
import {Invitation} from "./Invitation";
import {login} from "../utils/Login";
import NotFound from "./NotFound";
import {NotFound} from "./NotFound";
import {Impersonating} from "../components/Impersonating";
import RefreshRoute from "./RefreshRoute";
import {InviteOnly} from "./InviteOnly";
Expand All @@ -27,6 +27,7 @@ import {Application} from "./Application";
import {System} from "./System";
import {flushSync} from "react-dom";
import {UserTokens} from "./UserTokens";
import {Busy} from "./Busy";


export const App = () => {
Expand Down Expand Up @@ -133,6 +134,7 @@ export const App = () => {
<Route path="login" element={<Login/>}/>
<Route path="deadend" element={<InviteOnly/>}/>
<Route path="missingAttributes" element={<MissingAttributes/>}/>
<Route path="/home/login" element={<Busy/>}/>
<Route path="/*" element={<NotFound/>}/>
</Routes>}
</div>
Expand Down
10 changes: 10 additions & 0 deletions client/src/pages/Busy.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import "./NotFound.scss";
import React from "react";
import {Loader} from "@surfnet/sds";

export const Busy = () => {

return (
<Loader/>
);
}
14 changes: 8 additions & 6 deletions client/src/pages/NotFound.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import React from "react";
import NotFoundLogo from "../icons/undraw_page_not_found_re_e9o6.svg?url";
import I18n from "../locale/I18n";

const NotFound = () => (
<div className={"not-found"}>
<img src={NotFoundLogo} alt={I18n.t("notFound.alt")}/>
</div>
);
export default NotFound;
export const NotFound = () => {

return (
<div className={"not-found"}>
<img src={NotFoundLogo} alt={I18n.t("notFound.alt")}/>
</div>
);
}
5 changes: 4 additions & 1 deletion server/src/main/java/invite/api/RoleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import invite.repository.ApplicationRepository;
import invite.repository.ApplicationUsageRepository;
import invite.repository.RoleRepository;
import invite.repository.UserRoleRepository;
import invite.security.UserPermissions;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
Expand Down Expand Up @@ -212,7 +213,9 @@ public ResponseEntity<Void> deleteRole(@PathVariable("id") Long id,
}

provisioningService.deleteGroupRequest(role);
roleRepository.delete(role);
provisioningService.deleteUserRequest(role);
roleRepository.deleteRoleById(role.getId());

AccessLogger.role(LOG, Event.Deleted, user, role);
return Results.deleteResult();
}
Expand Down
8 changes: 7 additions & 1 deletion server/src/main/java/invite/api/UserRoleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import invite.exception.NotFoundException;
import invite.logging.AccessLogger;
import invite.logging.Event;
import invite.manage.ManageIdentifier;
import invite.model.*;
import invite.provision.ProvisioningService;
import invite.provision.scim.OperationType;
Expand Down Expand Up @@ -39,6 +40,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import static invite.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME;
import static invite.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME;
Expand Down Expand Up @@ -226,7 +228,8 @@ public ResponseEntity<Void> deleteUserRole(@PathVariable("id") Long id,
LOG.debug(String.format("DELETE user_roles/%s for user %s",id , user.getEduPersonPrincipalName()));
UserRole userRole = userRoleRepository.findById(id).orElseThrow(() -> new NotFoundException("UserRole not found"));
// Users are allowed to remove themselves from a role
if (!userRole.getUser().getId().equals(user.getId())) {
User userOfUserRole = userRole.getUser();
if (!userOfUserRole.getId().equals(user.getId())) {
UserPermissions.assertValidInvitation(user, isGuest ? Authority.GUEST : userRole.getAuthority(), List.of(userRole.getRole()));
}
if (userRole.isGuestRoleIncluded()) {
Expand All @@ -242,6 +245,9 @@ public ResponseEntity<Void> deleteUserRole(@PathVariable("id") Long id,
provisioningService.updateGroupRequest(userRole, OperationType.Remove);
provisioningService.deleteUserRoleRequest(userRole);
userRoleAuditService.logAction(userRole, UserRoleAudit.ActionType.DELETE);
// Deprovision the user for all provisionings which are exclusively used in this userRole
provisioningService.deleteUserRequest(userOfUserRole, userRole);

userRoleRepository.deleteUserRoleById(id);
AccessLogger.userRole(LOG, Event.Deleted, user, userRole);
}
Expand Down
11 changes: 10 additions & 1 deletion server/src/main/java/invite/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,16 @@ public void removeUserRole(UserRole role) {

@JsonIgnore
public Set<ManageIdentifier> manageIdentifierSet() {
return userRoles.stream()
return manageIdentifierSet(this.userRoles);
}

@JsonIgnore
public Set<ManageIdentifier> manageIdentifierSet(UserRole userRole) {
return manageIdentifierSet(Set.of(userRole));
}

private Set<ManageIdentifier> manageIdentifierSet(Set<UserRole> userRoleSet) {
return userRoleSet.stream()
.filter(userRole -> userRole.getAuthority().equals(Authority.GUEST) || userRole.isGuestRoleIncluded())
.map(userRole -> userRole.getRole().getApplicationUsages())
.flatMap(Collection::stream)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public interface ProvisioningService {

void deleteUserRequest(User user);

void deleteUserRequest(User user, UserRole userRole);

void deleteUserRequest(Role role);

void newGroupRequest(Role role);

void updateGroupRequest(UserRole userRole, OperationType operationType);
Expand Down
126 changes: 110 additions & 16 deletions server/src/main/java/invite/provision/ProvisioningServiceDefault.java
Original file line number Diff line number Diff line change
@@ -1,48 +1,79 @@
package invite.provision;

import com.fasterxml.jackson.databind.ObjectMapper;
import crypto.KeyStore;
import invite.eduid.EduID;
import invite.eduid.EduIDProvision;
import invite.exception.InvalidInputException;
import invite.exception.RemoteException;
import invite.manage.Manage;
import invite.manage.ManageIdentifier;
import invite.model.*;
import invite.model.Application;
import invite.model.Authority;
import invite.model.Provisionable;
import invite.model.RemoteProvisionedGroup;
import invite.model.RemoteProvisionedUser;
import invite.model.Role;
import invite.model.User;
import invite.model.UserRole;
import invite.provision.eva.EvaClient;
import invite.provision.graph.GraphClient;
import invite.provision.graph.GraphResponse;
import invite.provision.scim.*;
import invite.provision.scim.GroupPatchRequest;
import invite.provision.scim.GroupRequest;
import invite.provision.scim.GroupURN;
import invite.provision.scim.Member;
import invite.provision.scim.Operation;
import invite.provision.scim.OperationType;
import invite.provision.scim.UserRequest;
import invite.repository.RemoteProvisionedGroupRepository;
import invite.repository.RemoteProvisionedUserRepository;
import invite.repository.RoleRepository;
import invite.repository.UserRoleRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import crypto.KeyStore;
import lombok.Getter;
import lombok.SneakyThrows;
import okhttp3.OkHttpClient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.net.URI;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
@SuppressWarnings("unchecked")
public class ProvisioningServiceDefault implements ProvisioningService {

private final RoleRepository roleRepository;

private enum APIType {
USER_API("Users"), GROUP_API("Groups");
USER_API("Users"), GROUP_API("Groups");

@Getter
private final String display;
Expand Down Expand Up @@ -82,7 +113,7 @@ public ProvisioningServiceDefault(UserRoleRepository userRoleRepository,
EduID eduID,
@Value("${voot.group_urn_domain}") String groupUrnPrefix,
@Value("${config.eduid-idp-schac-home-organization}") String eduidIdpSchacHomeOrganization,
@Value("${config.server-url}") String serverBaseURL) {
@Value("${config.server-url}") String serverBaseURL, RoleRepository roleRepository) {
this.userRoleRepository = userRoleRepository;
this.remoteProvisionedUserRepository = remoteProvisionedUserRepository;
this.remoteProvisionedGroupRepository = remoteProvisionedGroupRepository;
Expand All @@ -93,11 +124,11 @@ public ProvisioningServiceDefault(UserRoleRepository userRoleRepository,
this.eduID = eduID;
this.graphClient = new GraphClient(serverBaseURL, eduidIdpSchacHomeOrganization, keyStore, objectMapper);
this.evaClient = new EvaClient(keyStore, remoteProvisionedUserRepository);
// Otherwise, we can't use method PATCH
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(1, TimeUnit.MINUTES);
builder.retryOnConnectionFailure(true);
restTemplate.setRequestFactory(new OkHttp3ClientHttpRequestFactory(builder.build()));
// Using JdkClientHttpRequestFactory (available in Spring 6.1+)
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory();
requestFactory.setReadTimeout(Duration.ofMinutes(1));
restTemplate.setRequestFactory(requestFactory);
this.roleRepository = roleRepository;
}

@Override
Expand Down Expand Up @@ -201,6 +232,63 @@ public void deleteUserRequest(User user) {

List<Provisioning> provisionings = getProvisionings(user);
//Delete the user to all provisionings in Manage where the user is known
deprovisionUser(user, provisionings);
}

@Override
public void deleteUserRequest(User user, UserRole userRole) {
//First send update role request
this.updateGroupRequest(userRole, OperationType.Remove);
/*
* We first need a List all provisionings for the user#userRole, and then we need to remove the provisiongs
* from that List that are in use by other user#userRoles, and those are the provisionings which we need to delete
*/
List<Provisioning> userRoleProvisionings = getProvisioningsUserRole(user, userRole);
List<String> otherProvisioningIdentifiers = user.getUserRoles().stream()
.filter(otherUserRole -> !otherUserRole.getId().equals(userRole.getId()))
.map(otherUserRole -> getProvisioningsUserRole(user, userRole))
.flatMap(Collection::stream)
.map(Provisioning::getId)
.toList();
List<Provisioning> provisionings = userRoleProvisionings.stream()
.filter(provisioning -> !otherProvisioningIdentifiers.contains(provisioning.getId()))
.toList();
//Delete the user to the not used anymore provisionings in Manage
deprovisionUser(user, provisionings);
}

@Override
public void deleteUserRequest(Role role) {
List<String> manageIdentifiers = getManageIdentifiers(role);
List<Provisioning> allRoleProvisionings = manage.provisioning(manageIdentifiers).stream()
.map(Provisioning::new)
.toList();
/*
* We can't deprovision all users of the Role in each provisioning, as they might be in use in other provisioned
* roles. We need all provisionings of the Role, but we need to check for each user which provisionings needs to
* be excluded from the deprovision.
*/
List<UserRole> userRoles = userRoleRepository.findByRole(role);
userRoles.forEach(userRole -> {
User user = userRole.getUser();
List<ManageIdentifier> otherManageIdentifiers = user.getUserRoles().stream()
.filter(otherUserRole -> !otherUserRole.getId().equals(userRole.getId()))
.map(otherUserRole -> user.manageIdentifierSet(userRole))
.flatMap(Collection::stream)
.toList();
// Provisionings that are used by any other userRoles are filtered out
List<Provisioning> provisionings = allRoleProvisionings.stream()
.filter(provisioning -> provisioning.getRemoteApplications().stream()
.noneMatch(otherManageIdentifiers::contains))
.toList();
//Delete the user to the not used anymore provisionings in Manage
deprovisionUser(user, provisionings);
});


}

private void deprovisionUser(User user, List<Provisioning> provisionings) {
provisionings.forEach(provisioning -> {
Optional<RemoteProvisionedUser> provisionedUserOptional = this.remoteProvisionedUserRepository
.findByManageProvisioningIdAndUser(provisioning.getId(), user);
Expand Down Expand Up @@ -465,6 +553,12 @@ private List<Provisioning> getProvisionings(User user) {
return manage.provisioning(identifiers).stream().map(Provisioning::new).toList();
}

private List<Provisioning> getProvisioningsUserRole(User user, UserRole userRole) {
Set<ManageIdentifier> manageIdentifiers = user.manageIdentifierSet(userRole);
List<String> identifiers = manageIdentifiers.stream().map(ManageIdentifier::manageId).toList();
return manage.provisioning(identifiers).stream().map(Provisioning::new).toList();
}

private List<Provisioning> getProvisionings(Role role) {
List<String> manageIdentifiers = getManageIdentifiers(role);
return manage.provisioning(manageIdentifiers).stream().map(Provisioning::new).toList();
Expand Down
9 changes: 9 additions & 0 deletions server/src/main/java/invite/repository/RoleRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;
Expand All @@ -16,6 +19,12 @@
@Repository
public interface RoleRepository extends JpaRepository<Role, Long>, QueryRewriter {

@Modifying
@Query(value = "DELETE FROM roles WHERE id = ?1", nativeQuery = true)
@Transactional(isolation = Isolation.SERIALIZABLE)
void deleteRoleById(Long id);


@Query(value = "SELECT *, (SELECT COUNT(*) FROM user_roles ur WHERE ur.role_id=r.id) as userRoleCount " +
"FROM roles r WHERE MATCH (name, description) against (?1 IN BOOLEAN MODE) AND id > 0 LIMIT ?2",
nativeQuery = true)
Expand Down
Loading
Loading