Skip to content

Commit

Permalink
WIP for ServiceDeskController #587
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Jan 15, 2025
1 parent ccbb1da commit 98d5406
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package myconext.api;

import lombok.Getter;
import myconext.exceptions.ForbiddenException;
import myconext.exceptions.UserNotFoundException;
import myconext.model.ControlCode;
import myconext.model.ExternalLinkedAccount;
import myconext.model.User;
import myconext.repository.UserRepository;
import myconext.security.UserAuthentication;
import myconext.verify.AttributeMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.Date;


@RestController
@RequestMapping(value = {"/myconext/api/servicedesk"})
Expand All @@ -22,16 +27,58 @@ public class ServiceDeskController implements UserAuthentication {

@Getter
private final UserRepository userRepository;
private final AttributeMapper attributeMapper;

public ServiceDeskController(UserRepository userRepository) {
public ServiceDeskController(UserRepository userRepository,
AttributeMapper attributeMapper) {
this.userRepository = userRepository;
this.attributeMapper = attributeMapper;
}

@PutMapping("")
public ResponseEntity<ControlCode> convertUserControlCode(Authentication authentication,
@RequestBody ControlCode controlCode) {
User serviceDeskMember = userFromAuthentication(authentication);
@GetMapping("/user/{code}")
public ResponseEntity<ControlCode> getUserControlCode(@PathVariable("code") String code) {
LOG.debug("Fetching user with controlCode: " + code);

User user = userRepository.findByControlCode_Code(code)
.orElseThrow(() -> new UserNotFoundException(String.format("No user found with controlCode %s", code)));
ControlCode controlCode = user.getControlCode();
controlCode.setUserUid(user.getUid());

return ResponseEntity.ok(controlCode);
}

@GetMapping("/validate")
public ResponseEntity<Boolean> validateDate(@RequestParam("dayOfBirth") String dayOfBirth) {
LOG.debug("Validate date: " + dayOfBirth);

Date date = AttributeMapper.parseDate(dayOfBirth);
return ResponseEntity.ok(date != null);
}

@PutMapping("/approve")
public ResponseEntity<Void> convertUserControlCode(Authentication authentication,
@RequestBody ControlCode controlCode) {
String code = controlCode.getCode();
User user = userRepository.findByControlCode_Code(code)
.orElseThrow(() -> new UserNotFoundException(String.format("No user found with controlCode %s", code)));

if (!user.getExternalLinkedAccounts().isEmpty() || !user.getLinkedAccounts().isEmpty()) {
throw new ForbiddenException("User has already linked-accounts: " + user.getEmail());
}
if (!user.getUid().equals(controlCode.getUserUid())) {
throw new ForbiddenException("User UID's do not match");
}

User serviceDeskMember = userFromAuthentication(authentication);

LOG.info(String.format("Adding external linked account for service desk for user %s by user %s",
user.getEmail(), serviceDeskMember.getEmail()));

ExternalLinkedAccount externalLinkedAccount = attributeMapper.createFromControlCode(controlCode);
user.getExternalLinkedAccounts().add(externalLinkedAccount);
userRepository.save(user);

return ResponseEntity.status(HttpStatus.CREATED).build();
}

}
10 changes: 6 additions & 4 deletions myconext-server/src/main/java/myconext/api/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.SneakyThrows;
import myconext.cron.DisposableEmailProviders;
Expand Down Expand Up @@ -48,9 +50,6 @@
import org.springframework.web.bind.annotation.*;
import tiqr.org.model.Registration;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
Expand Down Expand Up @@ -982,12 +981,15 @@ public ResponseEntity<StatusResponse> deleteUser(Authentication authentication,
@PostMapping("sp/control-code")
public ResponseEntity<ControlCode> createUserControlCode(Authentication authentication,
@RequestBody ControlCode controlCode) {
User user = userFromAuthentication(authentication);
if (!user.getExternalLinkedAccounts().isEmpty() || !user.getLinkedAccounts().isEmpty()) {
throw new ForbiddenException("User has already linked-accounts: " + user.getEmail());
}
String code = VerificationCodeGenerator.generateControlCode();
//Very small chance, but better be safe than sorry (User#ControlCode#code is indexed)
while (userRepository.findByControlCode_Code(code).isPresent()) {
code = VerificationCodeGenerator.generateControlCode();
}
User user = userFromAuthentication(authentication);
controlCode.setCode(code);
controlCode.setCreatedAt(System.currentTimeMillis());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import org.springframework.data.mongodb.core.index.Indexed;

import java.io.Serializable;
import java.time.Instant;

@Getter
@NoArgsConstructor
Expand All @@ -24,6 +23,9 @@ public class ControlCode implements Serializable {
@Setter
private long createdAt;

@Setter
private String userUid;

public ControlCode(String firstName, String lastName, String dayOfBirth) {
this.firstName = firstName;
this.lastName = lastName;
Expand Down
63 changes: 53 additions & 10 deletions myconext-server/src/main/java/myconext/verify/AttributeMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import myconext.manage.Manage;
import myconext.model.ExternalLinkedAccount;
import myconext.model.IdpScoping;
import myconext.model.Verification;
import myconext.model.VerifyIssuer;
import myconext.model.*;
import myconext.remotecreation.NewExternalEduID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
Expand All @@ -22,11 +19,7 @@
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalQueries;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

Expand Down Expand Up @@ -213,6 +206,57 @@ public ExternalLinkedAccount createExternalLinkedAccount(NewExternalEduID eduID,
return externalLinkedAccount;
}

public ExternalLinkedAccount createFromControlCode(ControlCode controlCode) {
String uuid = UUID.randomUUID().toString();
ExternalLinkedAccount externalLinkedAccount =
new ExternalLinkedAccount(
//String subjectId
uuid,
//IdpScoping idpScoping
IdpScoping.serviceDesk,
//VerifyIssuer issuer
new VerifyIssuer(IdpScoping.serviceDesk.name(), IdpScoping.serviceDesk.name(), null),
//Verification
Verification.Geverifieerd,
//String serviceUUID
uuid,
//String serviceID
uuid,
//String subjectIssuer
uuid,
//String brinCode
null,
//String initials
null,
//String chosenName
controlCode.getFirstName(),
//String firstName
controlCode.getFirstName(),
//String preferredLastName
controlCode.getLastName(),
//String legalLastName;
controlCode.getLastName(),
//String partnerLastNamePrefix
null,
//String legalLastNamePrefix
null,
//String preferredLastNamePrefix
null,
//String partnerLastName
null,
//Date dateOfBirth
parseDate(controlCode.getDayOfBirth()),
//Date createdAt
new Date(),
//Date expiresAt
Date.from(Instant.now().plus(DEFAULT_EXPIRATION_YEARS * 365, ChronoUnit.DAYS)),
//boolean external
true
);
externalLinkedAccount.setPreferred(true);
return externalLinkedAccount;
}

@SneakyThrows
public String serializeToBase64(VerifyState verifyState) {
Map<String, String> result = new HashMap<>();
Expand Down Expand Up @@ -258,7 +302,7 @@ public VerifyState serializeFromBase64(String base64) {

public static Date parseDate(String dateString) {
if (StringUtils.hasText(dateString)) {
for (DateTimeFormatter dateTimeFormatter: dateTimeFormatters) {
for (DateTimeFormatter dateTimeFormatter : dateTimeFormatters) {
try {
LocalDate localDate = LocalDate.parse(dateString, dateTimeFormatter);
return Date.from(localDate.atStartOfDay(ZoneOffset.UTC).toInstant());
Expand All @@ -281,7 +325,6 @@ public static List<String> externalAffiliations(List<String> brinCodes, Manage m
.toList();
}


private String getAttribute(Map<String, Object> attributes, String key) {
return (String) attributes.get(key);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,127 @@
package myconext.api;

import io.restassured.filter.Filter;
import io.restassured.filter.cookie.CookieFilter;
import myconext.AbstractIntegrationTest;
import myconext.model.ControlCode;
import myconext.model.ExternalLinkedAccount;
import myconext.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
"service_desk_role_auto_provisioning=True"
})
class ServiceDeskControllerTest extends AbstractIntegrationTest {

@Test
void getUserControlCode() {
ControlCode controlCode = given()
.when()
.pathParam("code", "12345")
.get("/myconext/api/servicedesk/user/{code}")
.as(ControlCode.class);
assertEquals("12345", controlCode.getCode());
assertEquals("mdoe", controlCode.getUserUid());
}

@Test
void getUserControlCodeNotFound() {
given()
.when()
.pathParam("code", "Nope")
.get("/myconext/api/servicedesk/user/{code}")
.then()
.statusCode(404);
}

@Test
void validateDate() {
Boolean valid = given()
.when()
.queryParam("dayOfBirth", "16 Mar 1981")
.get("/myconext/api/servicedesk/validate")
.as(Boolean.class);
assertTrue(valid);
}

@Test
void validateInvalidDate() {
Boolean valid = given()
.when()
.queryParam("dayOfBirth", "Nope")
.get("/myconext/api/servicedesk/validate")
.as(Boolean.class);
assertFalse(valid);
}


@Test
void convertUserControlCode() {
ControlCode controlCode = given()
.when()
.pathParam("code", "12345")
.get("/myconext/api/servicedesk/user/{code}")
.as(ControlCode.class);

given()
.when()
.body(controlCode)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.put("/myconext/api/servicedesk/approve")
.then()
.statusCode(201);

User user = userRepository.findOneUserByEmail("[email protected]");
assertEquals(1, user.getExternalLinkedAccounts().size());

ExternalLinkedAccount externalLinkedAccount = user.getExternalLinkedAccounts().getFirst();
DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder().appendPattern("dd-MM-yyyy").toFormatter();
ZonedDateTime dateOfBirth = externalLinkedAccount.getDateOfBirth().toInstant().atZone(ZoneId.systemDefault());
//See myconext-server/src/test/resources/users.json
assertEquals("11-03-1987", dateTimeFormatter.format(dateOfBirth));
}

@Test
void convertUserControlCodeForbidden() {
ControlCode controlCode = new ControlCode("firstName", "lastName", "dayOfBirth");
controlCode.setCode("12345");
controlCode.setUserUid("bogus");

given()
.when()
.body(controlCode)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.put("/myconext/api/servicedesk/approve")
.then()
.statusCode(403);
}

@Test
void convertUserControlCodeLinkedAccountsPresent() {
ControlCode controlCode = new ControlCode("firstName", "lastName", "dayOfBirth");
controlCode.setCode("54321");

User user = userRepository.findOneUserByEmail("[email protected]");
user.setControlCode(controlCode);
userRepository.save(user);

given()
.when()
.body(controlCode)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.put("/myconext/api/servicedesk/approve")
.then()
.statusCode(403);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
})
class ServiceDeskControllerUnauthorizedTest extends AbstractIntegrationTest {

// @Test
// void createUserControlCode() {
// given()
// .when()
// .get("/myconext/api/servicedesk")
// .then()
// .statusCode(403);
// }
@Test
void getUserControlCode() {
given()
.when()
.get("/myconext/api/servicedesk/user/12345")
.then()
.statusCode(403);
}
}
Loading

0 comments on commit 98d5406

Please sign in to comment.