Skip to content

Commit

Permalink
Endpoint for provisioning user with roles without invitation
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Nov 30, 2023
1 parent 15df08f commit b9c3452
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 14 deletions.
64 changes: 61 additions & 3 deletions server/src/main/java/access/api/UserRoleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
import access.logging.AccessLogger;
import access.logging.Event;
import access.model.*;
import access.provision.ProvisioningService;
import access.provision.graph.GraphResponse;
import access.provision.scim.OperationType;
import access.repository.RoleRepository;
import access.repository.UserRepository;
import access.repository.UserRoleRepository;
import access.security.UserPermissions;
import io.swagger.v3.oas.annotations.Parameter;
Expand All @@ -17,12 +21,13 @@
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.time.temporal.ChronoUnit;
import java.util.*;

import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME;
import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME;
Expand All @@ -39,11 +44,19 @@ public class UserRoleController {

private final UserRoleRepository userRoleRepository;
private final RoleRepository roleRepository;
private final UserRepository userRepository;
private final ProvisioningService provisioningService;
private final Config config;

public UserRoleController(UserRoleRepository userRoleRepository, RoleRepository roleRepository, Config config) {
public UserRoleController(UserRoleRepository userRoleRepository,
RoleRepository roleRepository,
UserRepository userRepository,
ProvisioningService provisioningService,
Config config) {
this.userRoleRepository = userRoleRepository;
this.roleRepository = roleRepository;
this.userRepository = userRepository;
this.provisioningService = provisioningService;
this.config = config;
}

Expand All @@ -58,6 +71,51 @@ public ResponseEntity<List<UserRole>> byRole(@PathVariable("roleId") Long roleId
return ResponseEntity.ok(userRoles);
}

@PostMapping("user_role_provisioning")
public ResponseEntity<Map<String, Integer>> userRoleProvisioning(@Validated @RequestBody UserRoleProvisioning userRoleProvisioning,
@Parameter(hidden = true) User apiUser) {
userRoleProvisioning.validate();
UserPermissions.assertInstitutionAdmin(apiUser);
List<Role> roles = userRoleProvisioning.roleIdentifiers.stream()
.map(roleId -> roleRepository.findById(roleId).orElseThrow(NotFoundException::new))
.toList();
UserPermissions.assertValidInvitation(apiUser, userRoleProvisioning.intendedAuthority, roles);
Optional<User> userOptional = Optional.empty();

if (StringUtils.hasText(userRoleProvisioning.sub)) {
userOptional = userRepository.findBySubIgnoreCase(userRoleProvisioning.sub);
} else if (StringUtils.hasText(userRoleProvisioning.eduPersonPrincipalName)) {
userOptional = userRepository.findByEduPersonPrincipalNameIgnoreCase(userRoleProvisioning.eduPersonPrincipalName);
} else if (StringUtils.hasText(userRoleProvisioning.email)) {
userOptional = userRepository.findByEmailIgnoreCase(userRoleProvisioning.email);
}
//Can't use shorthand notation as there are probably null values
User user = userOptional.orElseGet(() -> userRepository.save(new User(userRoleProvisioning)));

List<UserRole> newUserRoles = roles.stream()
.map(role ->
user.getUserRoles().stream()
.noneMatch(userRole -> userRole.getRole().getId().equals(role.getId())) ?
user.addUserRole(new UserRole(
apiUser.getName(),
user,
role,
userRoleProvisioning.intendedAuthority,
Instant.now().plus(role.getDefaultExpiryDays(), ChronoUnit.DAYS)))
: null)
.filter(Objects::nonNull)
.toList();

userRepository.save(user);
AccessLogger.user(LOG, Event.Created, user);

provisioningService.newUserRequest(user);
newUserRoles.forEach(userRole -> provisioningService.updateGroupRequest(userRole, OperationType.Add));

return Results.createResult();
}


@PutMapping("")
public ResponseEntity<Map<String, Integer>> updateUserRoleExpirationDate(@Validated @RequestBody UpdateUserRole updateUserRole,
@Parameter(hidden = true) User user) {
Expand Down
40 changes: 39 additions & 1 deletion server/src/main/java/access/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ public User(boolean superUser, Map<String, Object> attributes) {
this.nameInvariant(attributes);
}

public User(UserRoleProvisioning userRoleProvisioning) {
this.sub = resolveSub(userRoleProvisioning);
this.eduPersonPrincipalName = userRoleProvisioning.eduPersonPrincipalName;
this.schacHomeOrganization = userRoleProvisioning.schacHomeOrganization;
this.email = userRoleProvisioning.email;
this.name = userRoleProvisioning.name;
this.givenName = userRoleProvisioning.givenName;
this.familyName = userRoleProvisioning.familyName;
this.nameInvariant(Map.of(
"name", StringUtils.hasText(this.name)? this.name : "",
"preferred_username", ""
));
}

private void nameInvariant(Map<String, Object> attributes) {
String name = (String) attributes.get("name");
String preferredUsername = (String) attributes.get("preferred_username");
Expand Down Expand Up @@ -142,9 +156,10 @@ public User(boolean superUser, String eppn, String sub, String schacHomeOrganiza
}

@JsonIgnore
public void addUserRole(UserRole userRole) {
public UserRole addUserRole(UserRole userRole) {
this.userRoles.add(userRole);
userRole.setUser(this);
return userRole;
}

@JsonIgnore
Expand Down Expand Up @@ -203,4 +218,27 @@ public void updateRemoteAttributes(Map<String, Object> attributes) {
public Optional<UserRole> latestUserRole() {
return this.userRoles.stream().max(Comparator.comparing(UserRole::getCreatedAt));
}

@JsonIgnore
public String resolveSub(UserRoleProvisioning userRoleProvisioning) {
String schacHome = null;
String uid = null;
if (StringUtils.hasText(userRoleProvisioning.schacHomeOrganization)) {
schacHome = userRoleProvisioning.schacHomeOrganization;
}
String eppn = userRoleProvisioning.eduPersonPrincipalName;
if (StringUtils.hasText(eppn) && eppn.contains("@")) {
uid = eppn.substring(0, eppn.indexOf("@"));
schacHome = schacHome != null ? schacHome : eppn.substring(eppn.indexOf("@") + 1);
}
String mail = userRoleProvisioning.email;
if (StringUtils.hasText(mail)) {
uid = uid != null ? uid : mail.substring(0, mail.indexOf("@"));
schacHome = schacHome != null ? schacHome : mail.substring(mail.indexOf("@") + 1);
}
if (schacHome == null || uid == null) {
throw new IllegalArgumentException("Can't resolve sub from " + userRoleProvisioning);
}
return String.format("urn:collab:person:%s:%s", schacHome, uid);
}
}
33 changes: 33 additions & 0 deletions server/src/main/java/access/model/UserRoleProvisioning.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package access.model;

import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.util.StringUtils;

import java.util.List;

@ToString
@AllArgsConstructor
@NoArgsConstructor
public class UserRoleProvisioning {

@NotEmpty
public List<Long> roleIdentifiers;
public Authority intendedAuthority = Authority.GUEST;
public String sub;
public String email;
public String eduPersonPrincipalName;
public String givenName;
public String familyName;
public String name;
public String schacHomeOrganization;

public void validate() {
if (!StringUtils.hasText(email) && !StringUtils.hasText(eduPersonPrincipalName)) {
throw new IllegalArgumentException("Requires one off: email, eduPersonPrincipalName. Invalid userRoleProvisioning: " + this);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

@Service
@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -166,14 +167,14 @@ public void updateGroupRequest(UserRole userRole, OperationType operationType) {
userRoles = userRoleRepository.findByRole(userRole.getRole())
.stream()
.filter(userRoleDB -> userRoleDB.getAuthority().equals(Authority.GUEST) || userRoleDB.isGuestRoleIncluded())
.toList();
.collect(Collectors.toCollection(ArrayList::new));
boolean userRolePresent = userRoles.stream().anyMatch(dbUserRole -> dbUserRole.getId().equals(userRole.getId()));
if (operationType.equals(OperationType.Add) && !userRolePresent) {
userRoles.add(userRole);
} else if (operationType.equals(OperationType.Remove) && userRolePresent) {
userRoles = userRoles.stream()
.filter(dbUserRole -> !dbUserRole.getId().equals(userRole.getId()))
.toList();
.collect(Collectors.toCollection(ArrayList::new));
}
} else {
userRoles.add(userRole);
Expand Down
4 changes: 3 additions & 1 deletion server/src/main/java/access/repository/UserRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ public interface UserRepository extends JpaRepository<User, Long> {

List<User> findByOrganizationGUIDAndInstitutionAdmin(String organizationGUID, boolean institutionAdmin);

List<User> findByUserRoles_role_id(Long roleId);
Optional<User> findByEduPersonPrincipalNameIgnoreCase(String eppn);

Optional<User> findByEmailIgnoreCase(String email);

List<User> findByLastActivityBefore(Instant instant);

Expand Down
55 changes: 48 additions & 7 deletions server/src/test/java/access/api/UserRoleControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import access.AbstractTest;
import access.AccessCookieFilter;
import access.model.Authority;
import access.model.Role;
import access.model.UpdateUserRole;
import access.model.UserRole;
import access.exception.NotFoundException;
import access.manage.EntityType;
import access.model.*;
import io.restassured.common.mapper.TypeRef;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
Expand All @@ -14,10 +13,11 @@
import java.time.temporal.ChronoUnit;
import java.util.List;

import static access.Seed.INVITER_SUB;
import static access.Seed.MANAGE_SUB;
import static access.Seed.*;
import static access.security.SecurityConfig.API_TOKEN_HEADER;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

class UserRoleControllerTest extends AbstractTest {

Expand Down Expand Up @@ -120,4 +120,45 @@ void deleteUserRoleNotAllowed() throws Exception {
.then()
.statusCode(403);
}

@Test
void userRoleProvisioning() throws Exception {
super.stubForManagerProvidersByIdIn(EntityType.SAML20_SP, List.of("1", "2"));
super.stubForManageProviderByOrganisationGUID(ORGANISATION_GUID);
super.stubForManageProvisioning(List.of("1", "2"));

super.stubForCreateScimUser();
super.stubForCreateGraphUser();
super.stubForCreateScimRole();
super.stubForUpdateScimRole();

List<Long> roleIdentifiers = List.of(
roleRepository.findByName("Network").get(0).getId(),
roleRepository.findByName("Wiki").get(0).getId()
);
UserRoleProvisioning userRoleProvisioning = new UserRoleProvisioning(
roleIdentifiers,
Authority.GUEST,
null,
"[email protected]",
null,
null,
null,
"Charly Green",
null
);
given()
.when()
.header(API_TOKEN_HEADER, API_TOKEN_HASH)
.accept(ContentType.JSON)
.contentType(ContentType.JSON)
.body(userRoleProvisioning)
.post("/api/external/v1/user_roles/user_role_provisioning")
.then()
.statusCode(201);

User user = userRepository.findBySubIgnoreCase("urn:collab:person:domain.org:new_user").orElseThrow(NotFoundException::new);
assertEquals(2, user.getUserRoles().size());
}

}

0 comments on commit b9c3452

Please sign in to comment.